# Errors and debugging

> The error envelope, every status code and error type, which errors to retry, and how to track down a failing request.

Source: https://0.0.0.0:3000/docs/api-reference/errors
Documentation index: https://0.0.0.0:3000/docs/llms.txt

A request that fails before any output returns a non-`2xx` status and a JSON error envelope. A stream that fails after it has started reports the error [inside the stream](/docs/features/streaming#errors-after-streaming-starts).

## Error envelope

```json
{
  "error": {
    "code": 402,
    "message": "insufficient credits",
    "metadata": { "error_type": "insufficient_credits" }
  }
}
```

- Before streaming starts, the HTTP status equals `error.code`. After it starts, HTTP stays `200`; read the terminal chunk's `error.code`.
- `error.metadata.error_type` is the stable category. Branch on the status and `error_type`, never on `message`, which can change.
- A `400 invalid_request` on a chat completion names the field and the rule it broke, for example `n must be 1; multiple choices are not supported` or `n_predict must use the admitted chat messages or output limit fields`. A request too long for every endpoint tried returns `request exceeds the model's context length; shorten the input or lower the output limit`. Messages never repeat values you sent.
- An endpoint that cannot accept your tool, conversation or embedding input can fall back to another eligible endpoint. If none accepts it, you get `400 invalid_request`. A nonnumeric penalty or sampler such as `min_p: "low"` returns 400 before dispatch, as does a reasoning `effort` that no endpoint of the model accepts ([Reasoning](/docs/features/reasoning#routing-requirements)). A malformed encrypted envelope also returns 400. If an encrypted endpoint refuses your public output limit and it exceeds the approved limit, lower the named `max_tokens` or `max_completion_tokens` field.
- `500` messages are always `internal server error`.
- The request ID is in the `X-Request-Id` response header, not in the body. Error bodies never include `openrouter_metadata`.

## Status codes and error types

| Status | `error_type` | Meaning | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_request` | The body, a parameter or a header is invalid; or an unknown field, a reused `X-Request-Id`, media that can't be fetched or decoded, or a prompt too long for every endpoint tried. | Fix the request. Don't retry unchanged. |
| `400` | `interception_limit_exceeded` | The request is too large for your account's [request policy](/docs/privacy/request-policy) to process. | Shorten the conversation, or ask an owner to simplify the policy. |
| `401` | `invalid_credentials` | The key was refused. | Check the key and the route's key kind. See [Authentication](/docs/api-reference/authentication). |
| `402` | `insufficient_credits` | The request's cost can't be covered by your balance or spending limits. | See [402: insufficient credits](#402-insufficient-credits). |
| `403` | `forbidden` | Key management only: the management key's creator may not act on that key. | Use a key whose creator has the required organization role. |
| `403` | `input_blocked` | Your account's [request policy](/docs/privacy/request-policy) or the selected source's content policy blocked the input. The message tells you which. A source block can try another eligible endpoint if fallback is enabled. Nothing was charged. | Change the input. Don't retry unchanged. |
| `403` | `cors_forbidden` | A browser sent the request from a disallowed origin. | Call the API from your server. |
| `404` | `not_found` | The model (endpoints list), key or generation doesn't exist for your account. | Check the ID. |
| `408` | `timeout` | The request reached its time limit, including after partial output. | Retry once, with a shorter prompt or a smaller output limit. Handle any partial output and completed tool effects before retrying. |
| `408` | `cancelled` | The caller cancelled the request or response delivery failed. | Respect cancellation; start a new request only if you still need the result. |
| `413` | `payload_too_large` | The body exceeds the route's [body limit](/docs/api-reference/limits#request-body-size). | Shrink the body. |
| `415` | `unsupported_media_type` | A JSON route received a body without `Content-Type: application/json`. | Set the header. |
| `429` | `rate_limited` | A rate limit was reached. | See [429: rate limits](#429-rate-limits). |
| `500` | `server` | Unexpected error. | Retry with backoff. If it persists, contact support with the request ID. |
| `500` | `interception_failed` | Your account's request policy couldn't process this request or its response. | Contact support with the request ID. |
| `502` | `provider_unavailable` | Providers returned invalid responses or rejected the request upstream, and no fallback succeeded. | Retry with backoff. If it persists, check the request against the model's `supported_parameters` or set `provider.require_parameters: true`. |
| `503` | `provider_unavailable` | No endpoint can serve this request, or server execution was interrupted while you remained connected. | See [503: unavailable](#503-unavailable). |
| `503` | `provider_overloaded` | Matching endpoints have no free capacity. | Retry with backoff, or relax `provider` filters. |
| `503` | `bitcoin_reference_price_unavailable` | A BTC price couldn't be confirmed. The request wasn't sent or charged. | Retry with backoff. |
| `503` | `routing_unavailable`, `service_unavailable`, `catalogue_unavailable`, `authentication_unavailable`, `rate_limit_unavailable`, `interception_unavailable` | The service, or the part named, is temporarily unavailable. | Retry with backoff. |

An unknown path returns a plain-text `404`, and a known path with the wrong method returns `405` with an `Allow` header; neither uses the envelope. Check the path and method against the [endpoint list](/docs/api-reference/overview#endpoints).

### 402: insufficient credits

Before a request runs, an upper bound of its cost is held against your balance and spending limits, and unused funds are released when it completes. For token-priced models the hold covers the endpoint's full context and output limits, so lowering `max_completion_tokens` doesn't shrink it. A `402` means the hold didn't fit one of these:

| Limit | How to check |
| --- | --- |
| Available balance: your balance minus holds for other running requests | [Billing](/dashboard/billing) |
| The key's spending limit for the current period | `GET /key` → `limit_remaining` |
| The account's spending limit for this currency | [Billing](/dashboard/billing) |

Add credits, raise the limit, wait for running requests to finish, or choose a model with a lower worst-case cost. See [Limits](/docs/api-reference/limits#credits-and-reservations).

### 429: rate limits

| `Retry-After` | Message | Cause |
| --- | --- | --- |
| Yes | `rate limit reached` | Your key, account or key creator reached a rate limit. |
| Yes | `too many authentication attempts` | Too many failed authentications. |
| No | `rate limit reached` | Every endpoint tried was rate-limited by its provider before any output. |

Mid-stream, a provider rate limit arrives as an in-band `rate_limit_exceeded` error instead.

### 503: unavailable

`provider_unavailable` is the most common routing failure: no endpoint could serve this exact request, or every endpoint tried failed before producing output. Causes, most common first:

- The model ID is unknown, not published, not priced in your key's currency, or shared with an organization you can't access. These cases all return the same response, so it doesn't reveal whether an inaccessible model exists.
- No endpoint supports a feature you asked for: tools, JSON output, reasoning controls, a cache directive that must be honored, or a media input. `cache_control` on messages or text parts doesn't cause this unless you set `provider.require_parameters: true`.
- `provider.require_parameters` is `true` and no endpoint declares every sampler you sent.
- Your `provider` filters (`only`, `ignore`, `quantizations`, `regions`, `zdr`, `data_collection`, `max_price`) exclude every endpoint.
- Your `max_completion_tokens` exceeds every endpoint's maximum output.

An execution interruption while your connection remains open also returns `503 provider_unavailable`, with message `service temporarily unavailable`. This can happen mid-stream, where the error is in the final event and HTTP remains `200`. Your own cancellation remains `408 cancelled`.

When a deadline expires and cleanup also cancels response delivery, the terminal
error remains `408 timeout`. The cleanup cancellation does not replace the
original timeout with `408 cancelled`.

`provider_overloaded` is narrower: endpoints match, but none has free capacity. Retry after a short backoff.

An endpoint can also be connected but fail to open its separate request
connection before the attachment deadline. This returns `503 provider_unavailable`
if no eligible fallback succeeds. A request that wasn't sent to the provider
isn't charged. Contact support with the request ID if this repeats.

## Retry-After

`Retry-After` is set only on `429 rate_limited` responses caused by your rate limits or by repeated failed authentications. The value is whole seconds, at least 1. Wait at least that long before the next request with the same key. For other retryable errors, use exponential backoff.

## Errors in streams

Once a stream has started, its status is `200`. A later failure arrives as a final chunk with a top-level `error` object, `finish_reason: "error"` and then `data: [DONE]`, with no usage chunk. [Streaming](/docs/features/streaming#errors-after-streaming-starts) lists the in-band error types.

A client can retry a transient stream failure as a new request, but must handle the partial answer explicitly. Don't append a restarted answer to the partial output or replay tool effects blindly. The failed generation may have a charge; the retry is a separate generation with its own usage and cost.

## Request and generation IDs

Every response carries `X-Request-Id`. Inference responses also carry `X-Generation-Id` once output starts; an error before any output has none.

- **For support**, quote the `X-Request-Id`. Never send the key or the prompt.
- **For usage and cost**, pass the generation ID to [`GET /generation`](/docs/api-reference/chat/get-generation). It's the authoritative record after a dropped connection or a mid-stream error.
- **Use a fresh `X-Request-Id`** on every request, or omit it. IDs must be unique in your account, including for requests that failed: reusing one returns `400 invalid_request`.

## Debugging checklist

1. **Read the status and `error_type`.** They narrow the cause to one row above. For a `400`, the message names the field to fix.
2. **Check the body against [Parameters](/docs/api-reference/parameters).** Most `400`s come from reserved fields the API doesn't forward, such as `n` other than `1`, `plugins`, `store` or native output-limit spellings such as `n_predict`. Other common causes: a `models` array with more than one entry, a `:free` or `:nitro` suffix, and `max_tokens` of 0 or above the ceiling. Ordinary generation settings the API doesn't recognize are forwarded; the upstream decides whether it supports them.
3. **Confirm the model is available to your key.** [`GET /models`](/docs/api-reference/models/list-models) lists only models with an endpoint priced in your key's currency. [`GET /models/{author}/{slug}/endpoints`](/docs/api-reference/models/list-model-endpoints) shows each endpoint's `context_length`, `max_completion_tokens` and `supported_parameters`.
4. **Check `require_parameters`.** If you set it, compare every sampler you send with the endpoints' `supported_parameters`.
5. **Check prompt and output size.** Trim the prompt or lower `max_completion_tokens` to fit the endpoints' limits.
6. **For a `402`, check credits and spending limits,** including holds for other running requests.
7. **Relax routing filters one at a time** to find the one that excludes every endpoint.
8. **Look up the generation.** If output started, `GET /generation` shows the outcome of each endpoint that was tried.

## Retry strategy

For errors before streaming starts, retry only those that can succeed on a second try:

| Retry | Don't retry |
| --- | --- |
| `429`, `500`, `502`, `503`; `408 timeout` once | `400`, `401`, `402`, `403`, `404`, `413`, `415`; `408 cancelled` |

Use exponential backoff with jitter, honor `Retry-After`, cap the number of tries, and send a new `X-Request-Id` on every try, or none.

:::code-group
```python title="Python"
import os
import random
import time
import uuid

import requests

RETRYABLE = {429, 500, 502, 503}


class APIError(Exception):
    def __init__(self, status, error_type, request_id):
        super().__init__(f"{status} {error_type} (request {request_id})")
        self.status, self.error_type, self.request_id = status, error_type, request_id


def create_completion(body, max_attempts=5, base_delay=0.5, max_delay=30.0):
    for attempt in range(max_attempts):
        response = requests.post(
            "https://api.numes.ai/api/v1/chat/completions",
            headers={
                "Authorization": f"Bearer {os.environ['NUMES_API_KEY']}",
                # A fresh correlation ID per try, including retries.
                "X-Request-Id": f"summary-{uuid.uuid4().hex}",
            },
            json=body,
            timeout=630,
        )
        if response.ok:
            return response.json()

        try:
            error_type = response.json()["error"]["metadata"]["error_type"]
        except (ValueError, KeyError):
            error_type = "unknown"
        retryable = response.status_code in RETRYABLE or (
            response.status_code == 408 and error_type == "timeout" and attempt == 0
        )
        if not retryable or attempt == max_attempts - 1:
            raise APIError(response.status_code, error_type, response.headers.get("X-Request-Id"))

        retry_after = response.headers.get("Retry-After")
        if retry_after is not None:
            delay = float(retry_after)
        else:
            # Full jitter: a random delay up to the exponential ceiling.
            delay = random.uniform(0, min(max_delay, base_delay * 2**attempt))
        time.sleep(delay)
```

```typescript title="TypeScript"
const RETRYABLE = new Set([429, 500, 502, 503]);

class APIError extends Error {
  constructor(
    readonly status: number,
    readonly errorType: string,
    readonly requestId: string | null,
  ) {
    super(`${status} ${errorType} (request ${requestId})`);
  }
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export async function createCompletion(
  body: Record<string, unknown>,
  { maxAttempts = 5, baseDelayMs = 500, maxDelayMs = 30_000 } = {},
): Promise<unknown> {
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const response = await fetch("https://api.numes.ai/api/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.NUMES_API_KEY}`,
        "Content-Type": "application/json",
        // A fresh correlation ID per try, including retries.
        "X-Request-Id": `summary-${crypto.randomUUID()}`,
      },
      body: JSON.stringify(body),
    });
    if (response.ok) return response.json();

    const payload = await response.json().catch(() => undefined);
    const errorType: string = payload?.error?.metadata?.error_type ?? "unknown";
    const retryable =
      RETRYABLE.has(response.status) || (response.status === 408 && errorType === "timeout" && attempt === 0);
    if (!retryable || attempt === maxAttempts - 1) {
      throw new APIError(response.status, errorType, response.headers.get("X-Request-Id"));
    }

    const retryAfter = response.headers.get("Retry-After");
    const delay = retryAfter
      ? Number(retryAfter) * 1000
      : Math.random() * Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
    await sleep(delay);
  }
  throw new Error("unreachable");
}
```
:::

The OpenAI SDKs retry some of these statuses themselves and resend the same headers each time. If you set `X-Request-Id` through the SDK's default headers, turn off SDK retries (`max_retries=0` in Python, `maxRetries: 0` in TypeScript) and use a loop like the one above, or leave `X-Request-Id` unset.

## Next steps

:::cards
- {gauge} [Limits](/docs/api-reference/limits) Rate limits, body sizes and spending limits.
- {waves} [Streaming](/docs/features/streaming) In-band errors and how streams end.
- {route} [Fallbacks](/docs/routing/fallbacks) How requests move between endpoints before output.
- {receipt} [Usage accounting](/docs/account/usage-accounting) How usage and cost are recorded.
:::

## Dashboard forwarding failures

Dashboard requests can return `504 upstream_timeout` when the application service
does not confirm a response before the forwarding deadline, or
`503 upstream_unavailable` for another forwarding failure. These are dashboard errors,
separate from inference-provider availability. A failed mutation response does not
prove rollback: retry the original reviewed input with its saved receipt. Include
the request ID when reporting a persistent failure.
