What a 402 error means
The exact HTTP 402 contract FluxRouter returns: the status, every machine-readable refusal code, where each code is carried, and the body your client will actually receive. Measured against the live API.
An HTTP 402 (Payment Required) from FluxRouter means the request was refused on a money check rather than served. It is a safety limit, not a bug, and it is recoverable. This page publishes the exact contract: the status, the machine-readable code, where that code is carried, and the body your client receives.
Everything below was measured against the live API on 2026-08-04, run 449fdaf1d0e0c6f833f72e2e480d5407, with the raw bytes captured before any parsing. It is not read off our source.
The contract in one line
Every money refusal is HTTP 402 with a machine-readable code. There is no other status for a money refusal, and the status is stable — branch on the code, not on the message text, which we reserve the right to reword.
Two families, and they do not have the same shape
This is the part you need before you write a parser. FluxRouter has two independent money gates and, today, they carry the machine-readable code in two different places. A client that parses one and meets the other fails silently.
| family | what refuses | where the code is | body shape |
|---|---|---|---|
| Balance and caps | your prepaid balance, your self-set daily cap, your self-set monthly cap, an unreconciled charge | the X-Flux-Hold-Reason response header | error.message is a plain human sentence — there is no code in the body |
| Account gates | your account spend ceiling, plan and tier locks, the Free monthly cap, per-key lifetime caps, BYOK configuration | the body, at error.message and again at error.provider_specific_fields.error | error.provider_specific_fields carries structured detail |
We are stating our own inconsistency rather than smoothing it over, because a customer who writes one parser and meets the other family gets a silent failure. Read the header first and fall back to the body — the snippet below does exactly that, and it is correct for both families today and stays correct if we unify them later.
The exact bytes, family one — balance and caps
Status 402, header X-Flux-Hold-Reason: credit_exhausted, body:
{"error":{"message":"Your credit balance is exhausted. Top up to keep going. Top up at https://fluxrouter.ai/home/billing","type":"None","param":"None","code":"402"}}
Note code is the string "402" — it restates the status, it is not the refusal reason. The refusal reason is in the header.
The exact bytes, family two — account gates
Status 402, no X-Flux-Hold-Reason header, body:
{"error":{"message":"account_monthly_budget_exhausted","type":"None","param":"None","code":"402","provider_specific_fields":{"error":"account_monthly_budget_exhausted","account_max_budget_usd":0.01,"spent_usd_this_month":0.0,"inflight_usd":0.1,"message":"This account's $0.01 monthly cap is exhausted. Upgrade your plan or wait until the next billing cycle.","upgrade_url":"https://fluxrouter.ai/home/billing"}}}
Handling both in code
async function callFlux(body) {
const res = await fetch('https://api.fluxrouter.ai/v1/chat/completions', { /* ... */ });
if (res.status !== 402) return res;
const payload = await res.json();
// Family one puts the code in the header; family two puts it in the body.
const code =
res.headers.get('X-Flux-Hold-Reason') ??
payload?.error?.provider_specific_fields?.error ??
null;
switch (code) {
case 'credit_exhausted': return topUpBalance();
case 'daily_cap': return waitUntilUtcMidnight();
case 'monthly_cap': return raiseMonthlyCapOrWait();
case 'account_monthly_budget_exhausted': return requestHigherCeiling();
case 'free_tier_exhausted': return addAPaidPlan();
case 'credit_unresolved': return retryThenContactSupport();
default: return surfaceToOperator(code, payload);
}
}
Every refusal code
Four of these look alike and have four different remedies. An exhausted balance needs money. A reached daily cap needs time or a higher cap. A reached monthly cap needs a higher cap or the next cycle. A reached account ceiling is our runaway-cost limit and needs a ceiling increase, not a top-up — topping up will not clear it.
| code | family | carrier | what it means | what to do |
|---|---|---|---|---|
credit_exhausted | balance | header | Your prepaid balance cannot cover this request. | Top up. |
daily_cap | balance | header | Your own daily spend cap is reached. | Wait for midnight UTC, or raise the cap. |
monthly_cap | balance | header | Your own monthly spend cap is reached. | Raise the cap, or wait for the next cycle. |
credit_unresolved | balance | header | We could not verify your balance, or a prior charge is still being reconciled. | Retry once; if it persists, contact support. |
account_monthly_budget_exhausted | gate | body | Your account spend ceiling is reached. | Request a higher spend ceiling. Topping up does not clear this. |
spend_ceiling_unresolved | gate | body | We could not resolve a ceiling for this key, so we refuse rather than serve uncapped. | Contact support; the key needs a billing identity. |
free_tier_exhausted | gate | body | The Free plan's $1 monthly cap is reached. | Add a paid plan, or wait for the reset. |
premium_locked | gate | body | The model requires an active paid plan with a cleared payment. | Add or clear a plan. |
tier_locked | gate | body | The model is above your current trust tier. | The tier unlocks on the date in the body's unlock_at. |
key_lifetime_cap_exhausted | gate | body | This key's cumulative lifetime cap is reached. | Raise the cap on the key, or use another key. |
account_disputed | gate | body | The account is on hold pending a dispute or payment review. | Contact support. |
byok_no_capable_provider | gate | body | No provider you hold a key for can serve this model. | Add a key for a capable provider, or change model. |
byok_no_deployment | gate | body | That model has no bring-your-own-key deployment. | Use a BYOK-capable model, or a Flux-billed key. |
byok_no_account | gate | body | BYOK needs an account-scoped key. | Use an account-scoped key. |
byok_no_key | gate | body | No active key on file for that provider. | Add the provider key. |
The same table, machine-readable. This block is the published contract; our test suite fails if it and the set of codes the gateway can emit ever stop being the same set.
{"refusal_codes": [
{"code": "credit_exhausted", "http_status": 402, "family": "credit", "carrier": "header", "measured": true},
{"code": "credit_unresolved", "http_status": 402, "family": "credit", "carrier": "header", "measured": true},
{"code": "daily_cap", "http_status": 402, "family": "credit", "carrier": "header", "measured": true},
{"code": "monthly_cap", "http_status": 402, "family": "credit", "carrier": "header", "measured": true},
{"code": "account_monthly_budget_exhausted", "http_status": 402, "family": "gate", "carrier": "body", "measured": true},
{"code": "spend_ceiling_unresolved", "http_status": 402, "family": "gate", "carrier": "body", "measured": false},
{"code": "free_tier_exhausted", "http_status": 402, "family": "gate", "carrier": "body", "measured": false},
{"code": "premium_locked", "http_status": 402, "family": "gate", "carrier": "body", "measured": false},
{"code": "tier_locked", "http_status": 402, "family": "gate", "carrier": "body", "measured": false},
{"code": "key_lifetime_cap_exhausted", "http_status": 402, "family": "gate", "carrier": "body", "measured": false},
{"code": "account_disputed", "http_status": 402, "family": "gate", "carrier": "body", "measured": false},
{"code": "byok_no_capable_provider", "http_status": 402, "family": "gate", "carrier": "body", "measured": false},
{"code": "byok_no_deployment", "http_status": 402, "family": "gate", "carrier": "body", "measured": false},
{"code": "byok_no_account", "http_status": 402, "family": "gate", "carrier": "body", "measured": false},
{"code": "byok_no_key", "http_status": 402, "family": "gate", "carrier": "body", "measured": false}
]}
measured: true marks the five refusals we drove against the live API and captured on the wire for this page. The rest are published from the gateway's own refusal sites and carry the family's shape; we have not driven each one end to end, and we would rather say so than imply a measurement we did not take.
The cutoff window, and who pays for it
Every gateway has a gap between the instant a limit is reached and the instant the last request already in flight has settled, and every provider in this market leaks a little through it. What matters is not the leak. It is the surprise.
We measured ours. The largest single-request commitment against a limit on the live API was $0.10, and a settled cost became visible to the gate about 8 seconds after the response.
Any overshoot past your balance is ours. It is never carried forward, it never appears on a later invoice, and it never reduces a later top-up. We absorb it: the measured window above is $0.10 of commitment per request in flight, so a client running many requests at once can be a little further past the line at the moment it stops, and that difference is ours too.
{"cutoff_lag_policy": {
"policy": "absorb",
"carried_forward": false,
"appears_on_later_invoice": false,
"reduces_later_topup": false,
"measured_max_overshoot_usd": 0.10,
"measured_window_seconds": 8.09,
"measurement_source": "22-402-CONTRACT.json",
"customer_sentence": "Any overshoot past your balance is ours. It is never carried forward, it never appears on a later invoice, and it never reduces a later top-up."
}}
Fix
Pick the path that matches the code you received:
| If you received... | To resolve it |
|---|---|
free_tier_exhausted | Add a paid plan to keep going now, or wait for the monthly cap to reset. |
credit_exhausted | Top up your prepaid balance. |
daily_cap or monthly_cap | These are caps you set. Raise them in billing settings, or wait for the reset. |
account_monthly_budget_exhausted | Wait for the monthly reset, or request a higher ceiling. A top-up does not clear this. |
| Anything else, unexpectedly | Check your usage for a runaway loop or a leaked key (see below). |
1. Add a plan or upgrade
If you are on Free and need more than $1/month, choose a paid plan on the pricing page. Your API key does not change when you upgrade. See Plans and what's included.
2. Wait for the reset
Spend ceilings and monthly caps are per calendar month; daily caps reset at midnight UTC. If you can wait, your account resumes serving at the reset.
3. Request a higher ceiling
On paid plans the ceiling rises automatically over time with your cleared spend and account age, so for most accounts the answer is to keep using the platform and let it grow. If you have a near-term spike (a launch or a batch job), contact support or your account rep to discuss raising it ahead of schedule. See Spend ceilings and limits.
4. Rule out a runaway
If a 402 arrives sooner than expected, check your usage at /home/usage for unexpected volume. A retry loop, a misconfigured job, or a leaked API key can burn through a limit fast — capping that damage is exactly what these limits are for. Rotate the key in the dashboard if you suspect it leaked, then fix the source of the traffic. See Reading your usage and invoices.