Reference
Errors
Failures use the OpenAI error envelope, so SDK error handling works unchanged. Branch on error.code: it is stable, while messages may be reworded.
Error envelope (permalink)
Every error the gateway itself produces has this shape.
{
"error": {
"message": "Token budget exhausted. Contact your provider to renew.",
"type": "insufficient_quota",
"code": "token_budget_exhausted",
"param": null
}
}messageis human-readable and safe to log or show an operator. It never contains credentials.typeis the broad family:invalid_request_error,permission_error,insufficient_quotaorapi_error.codeis the specific reason. This is the field your code should switch on.paramis reserved for field-level errors and isnullon everything the gateway raises.
Gateway errors (permalink)
These nine are the complete set the gateway raises on its own. Anything else you see came from the model provider and passed straight through.
| Status | Code | Type | Meaning | What to do |
|---|---|---|---|---|
| 401 | missing_api_key | invalid_request_error | The request carried no credential at all. | Send Authorization: Bearer with your key, or the x-api-key header. |
| 401 | invalid_api_key | invalid_request_error | Unknown key, or a valid key sent to the other billing surface. | Check the key was copied whole, then confirm you are on the right base URL. |
| 403 | key_suspended | permission_error | The key exists but has been paused by whoever issued it. | Ask them to resume it. The key itself stays valid and does not need replacing. |
| 403 | key_expired | permission_error | The key is past its expiry date. | Request an extension or a new key. It will not recover on its own. |
| 403 | model_not_allowed | permission_error | The key is valid but is not scoped to the model in the request body. | Use a model on your allowlist, or ask for the allowlist to be widened. |
| 402 | insufficient_credit | insufficient_quota | A credit key whose USD balance has reached zero. | Ask for a top-up. Confirm the balance in the usage checker first. |
| 402 | token_budget_exhausted | insufficient_quota | A token key that has consumed its whole prepaid budget. | Ask for the budget to be renewed. |
| 404 | unknown_endpoint | invalid_request_error | Nothing followed the base URL, or the path did not resolve to a real endpoint. | Append a real path such as /chat/completions. Watch for a doubled or missing /v1. |
| 502 | upstream_unavailable | api_error | The gateway could not reach the model provider at all. Nothing was billed. | Retry with exponential backoff. Repeated 502s mean an outage, not a bad request. |
Order of checks (permalink)
Knowing the order tells you what a status rules out. The gateway stops at the first failing check and never reaches the ones below it.
- Path. A request with nothing after the base URL, or a path that does not resolve, gets
404 unknown_endpoint. This happens before authentication, so a 404 says nothing about your key. - Credential present. No
Authorization: Bearerand nox-api-keygives401 missing_api_key. - Key known. An unrecognised key gives
401 invalid_api_key. - Key usable. Suspended gives
403 key_suspended, past its expiry gives403 key_expired. - Right surface. A credit key on the token base URL, or the reverse, gives
401 invalid_api_keyagain. - Funds. An empty balance gives
402 insufficient_credit, a spent budget gives402 token_budget_exhausted. - Model allowed. A model outside the key’s allowlist gives
403 model_not_allowed. Only now is the request body read. - Forwarded. If the provider cannot be reached at all,
502 upstream_unavailable. Otherwise the provider’s own status is what you get.
Upstream failures (permalink)
A request that passes every check is forwarded to the model provider, and the provider’s status and response body come back to you unchanged. So a 400 about an unsupported parameter, a 429 about rate limits, or a 529 about capacity originates there rather than here, and the message reads in the provider’s own words.
Response headers are filtered rather than mirrored. content-type and x-request-id are preserved, streamed responses also carry cache-control: no-cache, no-transform, and everything else is dropped. Do not build logic on a header you have not seen documented here.
| Status | Where it comes from |
|---|---|
| 400, 404, 422 | The provider rejected the request body: a bad parameter, an unknown model id, an oversized context, or a missing max_tokens on an Anthropic Messages call. |
| 429 | The provider is rate limiting. The gateway does not add a rate limit of its own, so this always comes from upstream. Back off with jitter. |
| 5xx other than 502 | The provider failed or timed out. Retry with backoff, and report it if it persists. |
A 402 can also arrive this way, when the upstream account itself is short of funds. The distinction matters: if the usage checker shows your key still has balance or budget but calls keep returning 402, the problem is upstream and only your provider can clear it.
What to retry (permalink)
Authentication, permission and billing failures are decisions, not accidents. Retrying them wastes time and makes rate limits worse. Only 429 and 5xx deserve another attempt.
const response = await fetch(url, init);
if (!response.ok) {
const body = await response.json().catch(() => null);
const code = body?.error?.code ?? "unknown_error";
// Only 429 and 5xx are worth trying again.
const retryable = response.status === 429 || response.status >= 500;
throw Object.assign(new Error(body?.error?.message ?? response.statusText), {
status: response.status,
code,
retryable,
});
}Then retry only what is retryable, with exponential backoff and jitter.
async function withRetry(send, attempts = 4) {
for (let attempt = 0; ; attempt++) {
try {
return await send();
} catch (err) {
if (!err.retryable || attempt >= attempts - 1) throw err;
const wait = 500 * 2 ** attempt + Math.random() * 250; // jittered backoff
await new Promise((resolve) => setTimeout(resolve, wait));
}
}
}- Cap the attempts. Four tries over roughly ten seconds is plenty for a transient upstream fault.
- Add jitter. Without it, every client that failed at the same moment retries at the same moment.
- Stop on
402immediately, or you will hammer a key that has nothing left to spend.
What gets billed (permalink)
Failures cost nothing, but they are not all recorded the same way.
| Outcome | Billed | In the usage checker |
|---|---|---|
| Refused by the gateway | No | No. A 401, 402, 403 or 404 stops before the ledger, so it never appears as a request. |
| Forwarded, provider errored | No | Yes, recorded against your key as an error at zero cost. |
| Provider unreachable (502) | No | Yes, recorded as an error at zero cost. |
| Succeeded | Yes | Yes, with model, token counts and cost. |
Successful metadata calls are the one exception in the other direction. A GET /models reports no token usage, so it is neither billed nor listed. More detail on Checking usage.
Reporting a problem (permalink)
When something needs a human, include:
- The
x-request-idresponse header, when the provider supplied one. - The HTTP status and the whole
errorobject. - The key prefix only, meaning the first characters such as
kl_live_a1b2c3d4. Never send the whole key. - The model id, whether you were streaming, and roughly when it happened.