On this section
← All guides
Handle 402s and top-ups gracefully
The paywall anatomy, why nothing is ever half-charged, and the retry-after-payment pattern for scripts and agents.
Markdown mirror: /docs/guides/handle-402s.md
Every VendorStacks request is priced before it runs. If the key's balance can't cover it, the API returns 402 payment_required and charges nothing — there is no partial state to clean up, which makes the retry logic trivial.
Anatomy
Example
HTTP/1.1 402 Payment Required
X-Request-Id: 9f6f4a1e-…
{
"error": "payment_required",
"message": "This request costs 1 credit; balance is 0.",
"remaining_credits": 3,
"credits_needed": 10,
"topup_url": "https://vendorstacks.com/app/billing"
}- ›
credits_neededis the price of *this* request, not a suggested pack size. - ›
topup_urlgoes straight to billing — pass it to whoever holds the card. - ›The failed request is safe to retry byte-for-byte after top-up.
Script pattern: pause, poll, resume
retry-after-payment
async function callWithTopupPause(url, opts, key) {
for (;;) {
const res = await fetch(url, opts);
if (res.status !== 402) return res;
const { topup_url, credits_needed } = await res.json();
console.error(`Paused: need ${credits_needed} credits → ${topup_url}`);
// Poll the free balance endpoint until a top-up lands, then resume.
for (;;) {
await new Promise(r => setTimeout(r, 30_000));
const bal = await fetch("https://vendorradar-production.up.railway.app
/v1/balance", {
headers: { Authorization: `Bearer ${key}` },
}).then(r => r.json());
if (bal.credit_balance >= credits_needed) break;
}
}
}/v1/balance is free to call, so the poll loop costs nothing. For agents, don't poll — relay topup_url to the human and end the turn; see the agent guide. For batch jobs, checking /v1/pricing and /v1/balance up front lets you fail before doing half the work.