Error codes
Error response shapes, status codes and type values, per-endpoint differences, and which errors are safe to retry
This page is derived from what the code actually does; request and response headers, rate limits and balance handling are covered in Platform extensions. Two things first:
- A request passes through two layers: the gateway (authentication, rate limits, balance, body checks) and the routing layer (picking an upstream, calling it, failing over). The two layers do not produce errors in exactly the same shape, and the routing layer passes the upstream's status code through to you.
error.codeis not reliable. Gateway errors have nocode; routing-layer errors have acodethat sometimes equalstypeand sometimes is a finer value. Branch on the HTTP status pluserror.type, not oncode.
Every response carries an X-Request-ID header (echoed back if you sent one). Include it when you report a problem.
How to tell what happened
- Look at the status code.
2xxis success;4xxis usually a problem with the request and should not be retried as-is; only429and5xxare worth retrying (see Retries). - Look at the body shape (next section). Do not assume it is always
{"error": {...}}: on/v1/messagesit is an Anthropic-style object, and creation-time errors on native endpoints use the vendor's own format. - For streaming requests, look at the stream. A failure after HTTP
200has been sent only appears inside the stream; the status code does not change. See Failures in the middle of a stream.
Error shapes
OpenAI style (default)
/v1/chat/completions, /v1/responses, /v1/embeddings, images, audio, and every gateway-level error use this shape:
{
"error": {
"type": "invalid_request_error",
"message": "unknown model: foo-1"
}
}| Field | Notes |
|---|---|
error.type | Error category, see type values |
error.message | Human-readable text; do not parse it |
error.code | Absent on gateway errors. Present on routing-layer errors, where it may equal type or be a finer value (for example no_upstream_available). The 404 from GET /v1/models/{id} also carries param: null |
There is no error.param that points at the offending parameter; read message for the detail.
Anthropic style (/v1/messages and /v1/messages/count_tokens)
{
"type": "error",
"error": { "type": "invalid_request_error", "message": "max_tokens is required and must be > 0" }
}On these two endpoints, gateway errors (authentication, balance, rate limit, oversized body, suspended organization and so on) and routing-layer errors share this one shape, and type uses Anthropic's own names: 400 is invalid_request_error, 401 is authentication_error, 402 is billing_error, 403 is permission_error, 404 is not_found_error, 413 is request_too_large, 429 is rate_limit_error, 503 is overloaded_error (a type that is already an official name is kept), 504 is timeout_error, and any other 5xx is api_error.
Suspended organization or team
When the organization or team is suspended, or the key is not attached to an organization, the status is 403 and the body is the standard error object like every other gateway error, in the shape of the endpoint called (Anthropic style on /v1/messages, OpenAI style elsewhere, Google style on the Gemini endpoint). type is permission_error and message is organization suspended, team suspended or API key is not attached to an organization.
Creation-time errors on native endpoints
On native endpoints only the platform's own parameter validation uses the vendor's native format; every other error (see "Differences per endpoint" below) is still OpenAI style.
| Endpoint | Shape of a parameter-validation error |
|---|---|
/api/v1/services/aigc/* (DashScope: Wan, HappyHorse, Qwen Image) | {"code": "InvalidParameter", "message": "..."}; unknown task gives {"code": "NotFound", ...} |
/v1/contents/generations/tasks, /api/v3/contents/generations/tasks (Ark Seedance) | {"error": {"code": "InvalidParameter", "message": "..."}}; unknown task has code NotFound |
/v2/video_generation (MiniMax) | {"type": "error", "error": {"type": "bad_request_error", "message": "...", "http_code": "Bad Request"}}; unknown task has type not_found_error |
Status codes
| Status | Layer | When | What to do |
|---|---|---|---|
400 | gateway | No model can be read from the request (missing field, body is not valid JSON, or a chunked body over the size limit gets this same message); model is not currently callable (unknown model: ...) | Fix the body and model name; do not retry |
400 | routing | Body binding or validation failed (empty messages, invalid role, missing max_tokens on Anthropic, and so on); message has the reason | Fix the request per message |
400 | upstream | The upstream rejected the request (unsupported parameter, context too long, content moderation) | Fix the request per message |
401 | gateway | No Authorization: Bearer ...; malformed key (must start with sk-ms- or the legacy sk-relay- and be at least 20 characters); key does not exist, was revoked, or expired (these three return the same invalid API key) | Check the header and the key's status in the console. The x-api-key header is not accepted |
402 | gateway | Organization balance exhausted, or a budget envelope (key, member, team) is used up. Once the balance is exhausted, read-only queries (GET /v1/models, video task and response lookups and so on) keep working; only requests that cost money return 402 | Top up or adjust the budget, then retry |
403 | gateway | Organization or team suspended, or key not attached to an organization (permission_error); the key's model allow-list does not include the model (model_access_error) | Contact your organization admin; do not retry |
403 | upstream | The upstream returned 403, usually with type: permission_denied_error | Not a problem with your key; contact support with the X-Request-ID |
404 | gateway | GET /v1/models/{id} for a model that does not exist or is unpublished, code: model_not_found; /v1/videos* is retired, code: endpoint_retired | Check the model name / use the model's native endpoint |
404 | routing | The model exists but has no upstream configured (code: model_not_found); a video or response task ID that does not exist or has expired (not_found; native endpoints use their own formats) | Check the ID; tasks have a query window |
404 | framework | Unknown path or wrong method: the body is the plain text 404 page not found, not JSON | Check the path and method |
413 | gateway | The declared Content-Length exceeds the limit (gateway default 50 MB, configurable per deployment), invalid_request_error. The nginx in front of the public endpoint has its own 25 MB limit and answers 413 itself with an HTML body, not JSON | Shrink the body; avoid inlining large files as base64; plan for 25 MB or less |
422 | upstream | The platform does not generate 422 itself. If an upstream returns it, it is passed through as-is | Fix the request per message |
429 | gateway | RPM or TPM limit hit (rate_limit_error; message is always too many requests and does not say which limit), with Retry-After; exceeding concurrent Realtime sessions is also 429 (no Retry-After) | Back off per Retry-After |
429 | upstream | The upstream is rate limiting (including a model with a single upstream that is cooling down); carries Retry-After (the upstream's value when given, otherwise 5) | Back off by Retry-After |
429 | public entry | When one source IP sends requests too fast (about 30 per second, bursts of about 100 allowed), the nginx at the public entry answers 429 itself, with an HTML body and no Retry-After; this is separate from the key's RPM limit | Lower concurrency, back off exponentially |
500 | gateway | The gateway failed to build the upstream request (server_error) | Retry; contact support if it persists |
500 | routing | Any other unclassified internal error (internal_error); failure of a video or response task lookup or delete (an upstream 4xx there is also folded into this 500). message is always The request could not be completed; please retry later.; internal text is never echoed | Same |
502 | gateway | Cannot reach the routing layer, or no response headers from it within 120 seconds (server_error, message: upstream service unavailable) | Retry; for long non-streaming requests use stream: true |
502 | routing | Network-level failure to the upstream (connect, DNS, TLS, timeout): type: bad_gateway, message is always The upstream service is temporarily unavailable. Please retry. | Retry |
503 | gateway | See "Where a 503 comes from" below | See below |
503 | routing | No usable upstream: code: no_upstream_available; message is always The upstream service is temporarily unavailable. Please retry later.; carries Retry-After (default 5) | Retry later |
408, 504, and others | upstream | The upstream's own status. embeddings and rerank fold an upstream 408/504 into 408 timeout_error; other endpoints pass the status through unchanged | Retry |
Errors the routing layer returns for an upstream failure come from the last upstream it tried: the platform switches upstreams first, and when all fail it returns the last one's status and message. So the same 401, 402 or 403 can be either the gateway's verdict on your key or an upstream's verdict on the platform's own account. Gateway 401 messages are one of a few fixed sentences (see the table), and a gateway 402 always has type: insufficient_quota (billing_error on /v1/messages); anything that does not match is an upstream pass-through, covered below.
Where a 503 comes from
type | code | message looks like | Meaning | Retry-After |
|---|---|---|---|---|
overloaded_error | none | gateway at capacity, retry later | The gateway's concurrency cap is full (default 500 in-flight upstream requests, queueing up to 10 seconds) | yes, fixed 5 |
service_unavailable | none | service temporarily unavailable, please retry shortly | The gateway cannot currently guarantee that key revocations and suspensions take effect in time, so it refuses to serve (returned before authentication) | yes, fixed 5 |
api_error | none | the platform cannot evaluate this key right now | The gateway lacks the data it needs to decide on this key (just started, or sync incomplete) | no |
service_unavailable | no_upstream_available | The upstream service is temporarily unavailable. Please retry later. | The routing layer cannot pick an upstream | yes, default 5 (the routing layer's own wait when it has one) |
| the upstream's own type | varies | the upstream's text | The upstream returned 503 (an Anthropic-type upstream's 529 also becomes 503) | no |
All of these are worth retrying, but only the first states how long to wait.
type values
Generated by the gateway
type | Status | Notes |
|---|---|---|
authentication_error | 401 | Key missing, malformed or invalid |
model_access_error | 403 | The model is not in the allow-list of the key's organization or team |
insufficient_quota | 402 | Balance or budget envelope too low (billing_error on /v1/messages); message is insufficient balance, with (budget envelope: ...) appended when an envelope tripped |
rate_limit_error | 429 | RPM / TPM limit, or too many concurrent Realtime sessions |
invalid_request_error | 400 / 404 / 413 | Missing model, unknown model, oversized body, retired path |
overloaded_error | 503 | Gateway concurrency cap full |
service_unavailable | 503 | Configuration feed stalled |
api_error | 503 | Gateway cannot decide on this key |
server_error | 500 / 502 | Fault between the gateway and the routing layer |
Generated by the routing layer
type | Status | code | Notes |
|---|---|---|---|
invalid_request_error | 400 | same as type | Body binding or validation failed |
invalid_request_error | 404 | model_not_found | The model has no upstream |
service_unavailable | 503 | no_upstream_available | No usable upstream |
service_unavailable | 503 | upstream_cooling_down | All usable upstreams are in temporary cooldown; retry shortly |
bad_gateway | 502 | bad_gateway | Network-level upstream failure |
internal_error | 500 | internal_error | Unclassified internal error |
not_found | 404 | same as type | Response or video task ID not found |
The no_upstream_available and upstream_cooling_down codes were renamed on 2026-10-19. The old names no_suppliers_available and all_vendors_cooling_down mean the same thing; if your client branches on the old code, match both the old and the new name.
Upstream errors passed through the routing layer
The status is the upstream's; message is the upstream's text (with request IDs, internal endpoint IDs and similar identifiers stripped). type depends on the endpoint and the upstream family:
| Endpoint | Upstream status | type |
|---|---|---|
| chat / messages / responses, OpenAI-compatible upstreams (most domestic vendors) | any | api_error, status unchanged |
| chat / messages / responses, Anthropic-type upstreams | 400 401 403 404 429 500 | invalid_request_error authentication_error permission_denied_error not_found_error rate_limit_error internal_server_error; 529 becomes 503 service_unavailable; anything else api_error |
| image and video creation (the DashScope and Ark families) | 400 401/403 404 429 500 | invalid_request_error authentication_error (note that 403 is also mapped to 401) not_found_error rate_limit_error internal_server_error; anything else api_error |
| embeddings, rerank | by status and message text | rate_limit_error, context_length_exceeded, content_policy_violation, invalid_request_error, authentication_error, permission_denied_error, not_found_error, timeout_error (408), internal_server_error, service_unavailable, api_error |
context_length_exceeded and content_policy_violation only appear on embeddings and rerank. On conversational endpoints, an over-long context or a moderation block comes back as 400 with api_error or invalid_request_error; the reason is in message.
Differences per endpoint
| Endpoint | Gateway errors | Routing / upstream errors | Mid-stream failure |
|---|---|---|---|
/v1/chat/completions | OpenAI style | OpenAI style, with code | data: {"error":{"message","type"}} then data: [DONE] |
/v1/messages, /v1/messages/count_tokens | Anthropic style | Anthropic style | event: error with {"type":"error","error":{"type","message"}} |
/v1/responses | OpenAI style | OpenAI style, with code | event: error ({"type":"error","code","message"}) followed by event: response.failed |
/v1/embeddings, /v1/rerank, images, audio | OpenAI style | OpenAI style, with code | not applicable |
| Video creation and task polling (DashScope, Ark, MiniMax native endpoints) | OpenAI style | Parameter validation and unknown task use the native format; no upstream available or an upstream call failing is still OpenAI style | not applicable |
Things to know about video and other asynchronous tasks:
- A successful create call only means the task was submitted, with HTTP
200. If the task fails later, polling still returns200: on the DashScope endpointsoutput.task_statusisFAILEDand the reason is inoutput.codeandoutput.message; on the Ark and MiniMax endpoints the upstream's task body is returned withstatusset tofailed, and the error fields are the upstream's own. - A task ID that is invalid or expired on poll gives the native endpoint's own
404format; an error from the upstream call itself during a poll is an OpenAI-style500or502. /v1/videosand/v1/videos/generationsare retired and return404withcode: endpoint_retired. Video is served only on each model's native endpoint.
GET /v1/responses/{id} locates the upstream that created the response from the ID. An ID that cannot be decoded gives 400; an upstream that cannot be found gives 404 (type: not_found). DELETE /v1/responses/{id} and POST /v1/responses/{id}/cancel need no body and no model; the gateway passes them straight through. They cost nothing, so they keep working when the balance is exhausted.
Failures in the middle of a stream
Once a stream has started, the HTTP status is already 200, so later failures can only be written into the stream. Three cases:
- The upstream errors mid-stream, the connection drops, the stream is truncated (it ended without a terminal event), or no data arrives for 4 minutes by default: an error event is emitted in the endpoint's format from the table above, then the stream ends.
typeis the upstream's error type, andserver_errorfor network-level failures and truncation. - The organization's balance runs out while the stream is in progress: the gateway cuts the stream and writes
data: {"error":{"type":"insufficient_quota","message":"balance exhausted"}}followed bydata: [DONE]. This frame uses the OpenAI shape on every streaming endpoint, including/v1/messages. - You disconnect: no error event is sent.
How to handle it: treat "no normal terminator received" as a failure. For Chat Completions that means the last chunk has no finish_reason or an error frame appeared; for Anthropic, no message_stop; for Responses, no response.completed. On a mid-stream failure the part already generated is still billed at the upstream's actual usage.
If the failure happens before the first byte, the platform switches upstreams automatically. If one succeeds you get a normal response; only when all fail do you get a JSON error with a status code.
Retries
| Status | Retry? | Notes |
|---|---|---|
400 401 402 403 404 413 | No | Retrying unchanged gives the same result. 402 clears only after a top-up |
429 | Yes | Honor Retry-After if present, otherwise back off exponentially. Retry-After can be shorter than you actually need to wait when the TPM limit tripped; leave margin |
500 502 503 504 408 | Yes | Exponential backoff with jitter, a few attempts at most |
401 402 403 with type: api_error | No | This is an upstream's verdict on the platform's account, not your key or balance. Retrying does not help; contact support with the X-Request-ID |
import random, time
RETRYABLE = {408, 429, 500, 502, 503, 504}
def call_with_retry(send, tries=4):
for i in range(tries):
resp = send()
if resp.status_code not in RETRYABLE or i == tries - 1:
return resp
wait = float(resp.headers.get("Retry-After", 2 ** i))
time.sleep(wait + random.random())Duplicate calls and double billing
- The platform has no idempotency key and does not deduplicate by request content.
- When the platform switches upstreams internally, only the attempt that finally succeeds is billed; failed attempts are not. You see one success or one error.
- A retry from your side is a new request. If the earlier one actually succeeded and only the response failed to reach you (client timeout, dropped connection), the retry produces a second generation and a second charge. That is tolerable for conversational requests; it is not for video creation: after a timeout,
502or dropped connection on a create call, first check whether the task was created before creating another, or you end up with two tasks and two charges. - Do not retry a stream that failed midway as if it were the same generation: the output already delivered has been billed, and starting over bills again. To continue, put what you received into the context and send a new request.
- Non-streaming requests face two related time limits: the gateway disconnects and returns
502if it has no response headers after 120 seconds, and the routing layer's total budget for an upstream call is 10 minutes non-streaming and 5 minutes streaming. Usestream: truefor requests that may take longer than two minutes.
Models
GET /v1/models and GET /v1/models/{id}: query the models you can call, and how model IDs are used
Platform extensions
Platform-level behavior that does not depend on a model: request and response headers, automatic retries, rate limits, custom fields, and known differences from the official APIs