Errors
Every status code and error type the API returns, and what to do about each.
Errors come back in the OpenAI envelope:
{
"error": {
"message": "Insufficient balance. Please top up your account.",
"type": "insufficient_funds_error"
}
}Status codes
| Status | type | When you see it |
|---|---|---|
400 | invalid_request_error | Malformed body, missing model, or a capability the model cannot serve. |
401 | invalid_request_error | Key missing or invalid. Carries code: missing_api_key / invalid_api_key. |
402 | insufficient_funds_error | Your balance cannot cover the request. |
403 | tenant_unavailable_error | The account is suspended or unavailable. |
404 | invalid_request_error | Unknown model. |
409 | invalid_request_error | Idempotency conflict — a concurrent request with the same key is in flight. |
413 | invalid_request_error | Request body over 8 MiB. |
429 | rate_limit_error | Rate limit exceeded. Back off and retry. |
500 | internal_error | Our fault. Quote the request id when you tell us. |
503 | upstream_error | Temporarily unavailable, including when nothing can serve the model. Retry. |
504 | upstream_error | The model did not respond within five minutes. |
code
Alongside message and type, an error carries a stable machine-readable
code. It is the narrowest thing to branch on, and the only one that separates
errors sharing a status: unsupported_capability and upstream_rejected are
both a 400, and you do not handle them the same way.
code | Status | What it means |
|---|---|---|
missing_api_key | 401 | No Authorization header was sent. |
invalid_api_key | 401 | The key is unknown, expired or revoked. |
insufficient_funds | 402 | Your balance cannot cover the request. |
tenant_unavailable | 403 | The account is suspended or unavailable. |
unknown_model | 404 | No such model id. |
unsupported_capability | 400 | No backend serves this model with everything the request asks for. |
upstream_rejected | 400 | The backend refused the request itself — change it before resending. |
idempotency_conflict | 409 | A request with the same Idempotency-Key is still in flight. |
rate_limit_exceeded | 429 | Too many requests on this key. Back off. |
upstream_unavailable | 503 | Nothing could serve it just now. Retry. |
gateway_timeout | 504 | The model did not answer in time. |
not_routable | 500 | The model is listed but has no usable backend. Tell us. |
internal_error | 500 | Our fault. Quote the request id. |
Three errors come before `code` does
A body that cannot be parsed at all is rejected before any of this runs, so a
malformed JSON body, a missing model and an oversized body answer with
message and type alone.
Never branch on the wording of message. It is written for whoever is reading
a log, and it changes when that reads better.
Failover is automatic
A backend dropping out is not one of these. The request is retried against the equivalent model on OpenRouter before any error reaches you — see Failover & retries.
Which of these to retry
| Retry it | Fix it |
|---|---|
429 — with backoff | 400 — the request is wrong |
500 | 401 — the key is wrong |
503 | 402 — top up |
504 — if the work is worth it | 403 — talk to us |
404 — the model name is wrong | |
409 — you already sent this | |
413 — the request is too big |
Retries should be exponential and jittered — retrying immediately, or having every worker retry on the same schedule, turns one rejected request into a sustained wall of them.
Mid-stream errors are not statuses
Once a streamed response has begun, the status is already 200. A failure after
that arrives as an error event inside the stream. A client that does not look for
it will read a truncated answer as a complete one — see
Streaming.
Finding a request afterwards
Every request has an id, and it is the fastest thing to give us when something looks wrong. It also links a charge on your balance to the request that caused it, and carries the disposition explaining what happened. See Request dispositions and the usage log.