Errors
A request that fails answers with an HTTP status and a JSON error body. Every route uses OpenRouter's error shape, except /v1/messages, which uses Anthropic's so their SDKs and Claude Code can read it.
The error body
Every route but one answers errors in OpenRouter's shape. code repeats the HTTP status, message says what went wrong in words you can show a person, and metadata.error_type names the kind of error for code to branch on. type is there for OpenAI's SDKs, which read it.
{
"error": {
"code": 400,
"message": "Input for property `seed` must be a valid integer value",
"type": "validation_error",
"metadata": {
"error_type": "invalid_request"
}
}
}/v1/messages answers in Anthropic's shape instead, with Anthropic's type names:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "max_tokens is required and must be an integer of at least 1"
}
}Messages are written to be read. A validation error names the field it is about, and an error from the model's engine is cut to its first line, without the engine's internals.
Status codes
| Status | What it means | Retry? |
|---|---|---|
| 400 | A field is missing, malformed or not supported by the model. The message names it. | No: fix the request |
| 401 | The API key is missing, wrong or not active. | No |
| 404 | No such model, job, file or route. An unknown route's message names its method and path. | No |
| 405 | The route exists, but not for this method. Allow lists the methods it takes. |
No |
| 409 | A job changed state while you acted on it. | Read the job again first |
| 413 | The body or a file is larger than the route accepts. The message gives the limit. | No |
| 429 | Too many requests for this model. | Yes, after Retry-After |
| 500 | Something failed on our side. | Once, after a pause |
| 502 | The model's engine, or a service the request needs, could not be reached. | Yes, with a growing wait |
| 503 | The model is unavailable or busy right now. | Yes, with a growing wait |
| 504 | The engine did not answer in time. | Yes, with a growing wait |
Type names
Each error also names its kind, for code that branches on it: metadata.error_type and type in the usual envelope, and Anthropic's type on /v1/messages.
| Status | metadata.error_type |
type |
Anthropic type |
|---|---|---|---|
| 400 | invalid_request |
validation_error |
invalid_ |
| 401 | authentication |
auth_error |
authentication_error |
| 404 | not_found |
not_found_error |
not_found_error |
| 405 | invalid_request |
invalid_ |
invalid_ |
| 409 | invalid_request |
invalid_ |
|
| 413 | payload_too_large |
invalid_ |
request_too_large |
| 429 | rate_limit_exceeded |
rate_limit_error |
rate_limit_error |
| 500 | server |
server_error |
api_error |
| 502 | provider_unavailable |
server_error |
api_error |
| 503 | provider_overloaded |
server_error |
api_error |
| 504 | timeout |
timeout_error |
api_error |
A few errors carry a more specific metadata.error_type than their status: a video or song request that sends a field its model does not take is a 400 with unsupported_.
Validation errors are 400 on every route. The one exception is /v1/systemone, which answers 422 as TypeSafe's API does.
When a stream fails
A streamed request that fails before its first token answers with the real status and the error body above, so it can be handled like any other error.
Once a stream has started, its status has already been sent as 200, so a failure arrives in the stream:
Chat completions send one last chunk with the error, finish_reason: "error", then data: [DONE]:
{
"id": "chatcmpl-…",
"object": "chat.completion.chunk",
"created": 1790000000,
"model": "deepseek-v4.1-flash",
"error": {
"code": 504,
"message": "The AI service is taking too long to respond. Please try again later.",
"type": "timeout_error",
"metadata": {
"error_type": "timeout"
}
},
"choices": [
{
"index": 0,
"delta": {
"content": ""
},
"logprobs": null,
"finish_reason": "error",
"native_finish_reason": null
}
]
}Responses (/v1/responses) send an error event, then response.failed with the response and its error.
Messages (/v1/messages) send an error event with Anthropic's api_error, then message_stop.
Treat text already received as incomplete when a stream ends this way.
Retrying
How to wait, for the statuses the table marks as worth retrying:
- 429: as many seconds as
Retry-Aftersays. A request over the number a model runs at once waits its turn before it is refused, so a 429 can arrive after a pause. - 502, 503, 504: start at a second or two and double the wait each time.
- 500: once. If it fails again, the request is likely to keep failing.
Limits has a retry helper in Python and TypeScript.
A job created by POST /v1/videos or POST /v1/music that fails later is not an HTTP error: the job's status becomes failed and its error says why. See Async jobs.