Skip to content

Errors

Errors follow the OpenAI shape, with a nested error object:

{
"error": {
"message": "Model is currently at capacity. Please retry shortly.",
"type": "server_error",
"code": "model_overloaded"
}
}

Every error the API generates uses it, including authentication failures and insufficient balance — both of which previously returned a flat {"success": false, "error": "..."} object. error.message is always populated, so an SDK reading err.error.message never gets undefined.

Branch on error.code. error.type is deliberately coarser — OpenAI SDKs read it to decide whether a failure is retryable at all — so two codes that need opposite handling can share a type.

Status code Meaning Retry?
400 bad_request Malformed request, or an unsupported reasoning_effort for this model. No — fix the request
400 request_too_large Estimated tokens exceed the model’s entire budget, so no amount of waiting admits it. Lower max_tokens or shorten the prompt. No
400 body_read_failed The body stalled and could not be read. Yes, once
401 invalid_api_key Missing, malformed, revoked or expired API key. No
403 insufficient_permissions Your source IP is outside the key’s allowlist, or your user has no access to this workspace. No
403 model_not_entitled The model exists but your workspace’s plan does not include it. No
404 model_not_found Unknown model slug, or a model not available to you. On chat, completions and reranking this also covers a 404 from the engine. No
404 not_found A 404 relayed from the engine on embeddings, images, video or audio. No
408 request_timeout Timed out reading the request body — typically after a long queue wait. Carries Retry-After: 1. Yes
409 conflict Relayed from the model engine. No — fix the request
413 request_too_large A JSON request body over the 32 MB cap, or the engine rejected the payload size. See the note below for multipart uploads. No
422 INSUFFICIENT_BALANCE Workspace wallet is exhausted. No — top up
422 unprocessable_entity Relayed from the model engine: the body parsed but the engine would not act on it. No
429 rate_limit_exceeded You exceeded your workspace’s per-minute request limit. Yes — but slow down first
429 upstream_rate_limited A limit upstream of us was hit. Nothing about your tier or request is at fault. Yes — honour Retry-After
500 internal_error Unexpected server or upstream transport failure. Yes, with backoff
502 upstream_error We could not authenticate to the model engine. Our fault, not yours. No — contact us
503 service_unavailable The model has no active or healthy provider, or the API-key store is briefly unreachable. Yes, with backoff
503 model_overloaded Queued, but the model’s maximum wait elapsed. Retry-After: 5. Yes
503 queue_full The model’s queue is at its depth limit. Retry-After: 10. Yes
503 model_not_configured Model has no admission configuration. Retry-After: 30. No — contact us
503 queue_error Admission control failed unexpectedly. Yes
504 upstream_timeout The model never responded, or stopped emitting mid-stream. Yes, with backoff

Note that request_too_large appears on both 400 and 413, and they are not the same failure: 400 is about the token budget your request asks for, 413 is about the bytes you sent.

A 4xx from the model engine is relayed with its own status, and — with the two exceptions below — its own message, so a malformed tools payload surfaces the model’s own explanation rather than a generic 400.

The first carve-out. An upstream 401 or 403 is converted to 502 upstream_error with a generic message. The engine authenticates against our provider credential, not your API key — relaying it would tell you your key was invalid, or that you needed a plan upgrade, when neither is true. A 502 is always ours to fix, never yours.

The second. We relay the message only when the response body is a JSON error object we can read a message out of. Some 4xx responses never reach the engine at all — a gateway in front of it rejects an oversized body with its own 413 HTML page, for instance — and those keep the status but answer with a fixed message:

The request was rejected upstream. Please check your request and try again.

If you see that one, the status is still the honest thing to branch on, but the detail did not survive: send us the request ID and we will read it off our side.

5xx responses from the engine, connection failures, and a 200 whose body we cannot parse are reported as 500 internal_error, never with the engine’s own text. A request that times out or stops mid-transfer is 504 upstream_timeout.

Which status you get is identical on every endpoint: chat, completions, embeddings, reranking, images, video and audio all classify upstream failures the same way. The code and message differ on exactly one status.

An upstream 404 is the single status where two endpoint families answer differently, and if you branch on error.code you have to handle both:

Endpoints code Message
Chat, completions, reranking model_not_found Always Model not found: <your model>. The engine’s own text is discarded.
Embeddings, images, video, audio not_found The engine’s message, relayed.

The chat family answers through the same helper that makes a private model indistinguishable from a model that does not exist — a deliberate isolation property, so that one customer cannot discover another’s private models by reading error text. Discarding the engine’s message is the price of that guarantee, not an oversight. Treat both codes as the same class of failure: the model you asked for is not available to you.

{
"error": {
"message": "Insufficient balance. Your current balance is $0.00. Please top up your account to continue using the API.",
"type": "insufficient_quota",
"code": "INSUFFICIENT_BALANCE"
}
}

The 422 status and the INSUFFICIENT_BALANCE code are unchanged — if you key on that pair, nothing about your integration needs to move. Only the envelope changed: the message is now at error.message rather than at the top-level error.

This is checked before the model runs, so a request rejected this way costs nothing. Top up from Billing to resume.

There are two, they mean opposite things, and code is the only field that separates them — the status and the rate_limit_error type are identical.

rate_limit_exceeded — you are sending too fast.

{
"error": {
"message": "Rate limit exceeded. Please slow down your requests.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}

Your workspace’s per-minute allowance is spent. Retrying at the same rate will fail again: reduce concurrency or dispatch rate. Retry-After is the seconds remaining in the current window.

upstream_rate_limited — a limit upstream of us was hit.

{
"error": {
"message": "Request failed: server is overloaded",
"type": "rate_limit_error",
"code": "upstream_rate_limited"
}
}

This one tells you whose limit was hit, not how long it lasts. “Upstream” means anything past our API: a saturated model engine, the gateway in front of it, or our own account quota at a third-party provider. The first clears in seconds; the last may not clear for a while.

What you can rely on is the negative — nothing about your tier or your request is at fault, so cutting your request rate will not clear it. Retry the identical request, and let Retry-After set the pace rather than guessing. That header is the upstream’s own value when it sent a usable one, 5 when it did not, and is capped at 60.

The audio endpoints are the exception: /audio/transcriptions and /audio/translations do not carry the upstream’s value through, so a relayed 429 there always reports the 5 second default regardless of what the engine asked for. Treat it as a floor on those two endpoints, not as the engine’s own estimate.

Both always carry Retry-After in seconds. See Rate limits and queueing for the headers on every response and how to pace requests.

model_overloaded and queue_full both mean the model is busy rather than broken, and both are safe to retry — Retry-After is 5 and 10 seconds respectively. These are the two you should expect to see under load, and the queueing guide explains why.

  • Always honour Retry-After when present. It is a real signal, not a formality.
  • Retry 408, 429, 500, 503 and 504, plus 400 body_read_failed — that one is about the connection, not the request. Do not retry a 4xx that describes your request — 400 bad_request, 400 request_too_large, 403, 404, 409, 413, 422. They will fail identically.
  • On 429, check code before retrying: upstream_rate_limited wants the same request again, rate_limit_exceeded wants a smaller request rate.
  • Use exponential backoff with jitter for 500 and 504, so a transient upstream problem doesn’t turn into a synchronised retry storm.
  • Cap total attempts. A queued request may already have waited a long time before failing, so unbounded retries can multiply latency rather than fix it.