# 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.

```json
{
  "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:

```json
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "max_tokens is required and must be an integer of at least 1"
  }
}
```

> [!NOTE]
> 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_request_error` |
| 401 | `authentication` | `auth_error` | `authentication_error` |
| 404 | `not_found` | `not_found_error` | `not_found_error` |
| 405 | `invalid_request` | `invalid_request_error` | `invalid_request_error` |
| 409 | `invalid_request` | `invalid_request_error` | |
| 413 | `payload_too_large` | `invalid_request_error` | `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_parameter`.

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]`:

```json
{
  "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-After` says. 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](/docs/concepts/limits#retrying) 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](/docs/concepts/async-jobs#a-jobs-life).
