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.

familywhat refuseswhere the code isbody shape
Balance and capsyour prepaid balance, your self-set daily cap, your self-set monthly cap, an unreconciled chargethe X-Flux-Hold-Reason response headererror.message is a plain human sentence — there is no code in the body
Account gatesyour account spend ceiling, plan and tier locks, the Free monthly cap, per-key lifetime caps, BYOK configurationthe body, at error.message and again at error.provider_specific_fields.errorerror.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:

json
{"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:

json
{"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

js
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.

codefamilycarrierwhat it meanswhat to do
credit_exhaustedbalanceheaderYour prepaid balance cannot cover this request.Top up.
daily_capbalanceheaderYour own daily spend cap is reached.Wait for midnight UTC, or raise the cap.
monthly_capbalanceheaderYour own monthly spend cap is reached.Raise the cap, or wait for the next cycle.
credit_unresolvedbalanceheaderWe could not verify your balance, or a prior charge is still being reconciled.Retry once; if it persists, contact support.
account_monthly_budget_exhaustedgatebodyYour account spend ceiling is reached.Request a higher spend ceiling. Topping up does not clear this.
spend_ceiling_unresolvedgatebodyWe 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_exhaustedgatebodyThe Free plan's $1 monthly cap is reached.Add a paid plan, or wait for the reset.
premium_lockedgatebodyThe model requires an active paid plan with a cleared payment.Add or clear a plan.
tier_lockedgatebodyThe model is above your current trust tier.The tier unlocks on the date in the body's unlock_at.
key_lifetime_cap_exhaustedgatebodyThis key's cumulative lifetime cap is reached.Raise the cap on the key, or use another key.
account_disputedgatebodyThe account is on hold pending a dispute or payment review.Contact support.
byok_no_capable_providergatebodyNo provider you hold a key for can serve this model.Add a key for a capable provider, or change model.
byok_no_deploymentgatebodyThat model has no bring-your-own-key deployment.Use a BYOK-capable model, or a Flux-billed key.
byok_no_accountgatebodyBYOK needs an account-scoped key.Use an account-scoped key.
byok_no_keygatebodyNo 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.

json
{"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.

json
{"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_exhaustedAdd a paid plan to keep going now, or wait for the monthly cap to reset.
credit_exhaustedTop up your prepaid balance.
daily_cap or monthly_capThese are caps you set. Raise them in billing settings, or wait for the reset.
account_monthly_budget_exhaustedWait for the monthly reset, or request a higher ceiling. A top-up does not clear this.
Anything else, unexpectedlyCheck 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.