# Errors

> The error response format, and every error the API returns with its status, reason, an example and what to do.

Source: https://router.africa/docs/errors

Errors use one JSON shape. Branch on the HTTP status for retry logic and on `metadata.reason` for the specific cause. The exception is `POST /v1/messages`, which uses Anthropic's format. See [Errors on `POST /v1/messages`](#errors-on-post-v1messages).

```json
{
  "error": {
    "code": 402,
    "message": "This workspace doesn't have enough credits for this request. Add credits at https://app.router.africa.",
    "metadata": {
      "error_type": "payment_required",
      "reason": "insufficient_credits"
    }
  }
}
```

| Field | Meaning |
|---|---|
| `error.code` | The HTTP status, as a number. |
| `error.message` | A message you can show to a person. It never contains a full key, a provider name or any of your prompt. |
| `error.metadata.error_type` | The category of the error. |
| `error.metadata.reason` | The specific cause. Messages can change, so match on this, not on the message. |

The OpenAI and Anthropic SDKs choose the exception from the HTTP status and read `error.message`, so they work unchanged. `error.code` is a number, not a string.

## Headers

- `x-request-id` is on every response. Include it when you contact support.
- `WWW-Authenticate: Bearer realm="router.africa"` is on every `401`.
- `Retry-After` (seconds) is always sent on `429`, `503` and `529`. If no other value applies it is 5 seconds. See [Rate limits](https://router.africa/docs/rate-limits).
- `x-idempotent-replay: true` is sent only when the response is a stored replay of an earlier identical request. You aren't charged again. See [Idempotency and retries](https://router.africa/docs/idempotency-and-retries).
- Rate-limit headers such as `x-ratelimit-remaining` are not sent.

## Handle an error

**curl**

```bash
curl -i https://api.router.africa/v1/chat/completions \
  -H "Authorization: Bearer $ROUTER_AFRICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "openai/does-not-exist", "messages": [{"role": "user", "content": "Hello"}]}'

# HTTP/2 404
# x-request-id: req_8f2c91
```

**Python**

```python
import os

from openai import OpenAI

client = OpenAI(base_url="https://api.router.africa/v1", api_key=os.environ["ROUTER_AFRICA_API_KEY"])

try:
    client.chat.completions.create(
        model="openai/does-not-exist",
        messages=[{"role": "user", "content": "Hello"}],
    )
except openai.APIStatusError as e:
    print(e.status_code, e.body["metadata"]["reason"], e.response.headers.get("x-request-id"))
```

**TypeScript**

```typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.router.africa/v1",
  apiKey: process.env.ROUTER_AFRICA_API_KEY,
});

try {
  await client.chat.completions.create({
    model: "openai/does-not-exist",
    messages: [{ role: "user", content: "Hello" }],
  });
} catch (err) {
  if (err instanceof OpenAI.APIError) {
    console.log(err.status, err.error?.metadata?.reason, err.headers?.["x-request-id"]);
  }
}
```

## Errors while streaming

An error before the first token is a normal error response. An error after the stream has started can't change the `200` status. It arrives as a final `data: {"error": {…}}` event with the same body, and then the stream ends. On `POST /v1/messages` it is an `event: error` event in Anthropic's format. See [Streaming](https://router.africa/docs/streaming).

## Retrying

Retry `408`, `429`, `500`, `502`, `503` and `529` with backoff. For `409`, wait, then retry. Don't retry `400`, `401`, `402`, `403`, `404` or `413` without changing something first. See [Idempotency and retries](https://router.africa/docs/idempotency-and-retries).

A `max_tokens` (or `max_output_tokens`) above the model's limit is lowered to the limit. It isn't an error.

## Error reference

### `400` `invalid_body`

Body isn't valid JSON or fails validation. Error type: `invalid_request`. What to do: Fix the request. Don't retry.

```json
{
  "error": {
    "code": 400,
    "message": "Request body is invalid: messages must be a non-empty array.",
    "metadata": {
      "error_type": "invalid_request",
      "reason": "invalid_body"
    }
  }
}
```

### `400` `key_not_in_header`

A key was sent in the URL or the body. Error type: `invalid_request`. What to do: Move the key to the header. Rotate it.

```json
{
  "error": {
    "code": 400,
    "message": "Send your API key in the Authorization header, not the URL or body.",
    "metadata": {
      "error_type": "invalid_request",
      "reason": "key_not_in_header"
    }
  }
}
```

### `400` `context_length_exceeded`

The prompt is too long for the model. Error type: `context_length_exceeded`. What to do: Shorten the input.

```json
{
  "error": {
    "code": 400,
    "message": "The request is too long for openai/gpt-4o-mini's context window. Shorten the input and retry.",
    "metadata": {
      "error_type": "context_length_exceeded",
      "reason": "context_length_exceeded"
    }
  }
}
```

### `401` `missing_api_key`

No `Authorization` header, or it isn't Bearer. On `POST /v1/messages`, also no `x-api-key`. Error type: `authentication`. What to do: Add the header.

```json
{
  "error": {
    "code": 401,
    "message": "You didn't provide an API key. Send it as `Authorization: Bearer YOUR_KEY`.",
    "metadata": {
      "error_type": "authentication",
      "reason": "missing_api_key"
    }
  }
}
```

### `401` `invalid_api_key`

Key unknown, malformed, revoked, expired or not yet active. Error type: `authentication`. What to do: Use a valid key. Don't retry.

```json
{
  "error": {
    "code": 401,
    "message": "Incorrect API key provided: sk-ra-…abcd. Check your keys at https://app.router.africa.",
    "metadata": {
      "error_type": "authentication",
      "reason": "invalid_api_key"
    }
  }
}
```

### `402` `insufficient_credits`

Available credits are too low for the request. Error type: `payment_required`. What to do: Add credits. Don't retry until then.

```json
{
  "error": {
    "code": 402,
    "message": "This workspace doesn't have enough credits for this request. Add credits at https://app.router.africa.",
    "metadata": {
      "error_type": "payment_required",
      "reason": "insufficient_credits"
    }
  }
}
```

### `402` `key_spend_cap_exceeded`

The key reached its spend cap. Error type: `payment_required`. What to do: Raise the cap, wait for it to reset, or use another key.

```json
{
  "error": {
    "code": 402,
    "message": "This API key has reached its spend limit. Raise the limit or use another key.",
    "metadata": {
      "error_type": "payment_required",
      "reason": "key_spend_cap_exceeded"
    }
  }
}
```

### `403` `account_suspended`

The account or workspace is suspended. Error type: `permission_denied`. What to do: Contact support. Don't retry.

```json
{
  "error": {
    "code": 403,
    "message": "This account is suspended. Contact support.",
    "metadata": {
      "error_type": "permission_denied",
      "reason": "account_suspended"
    }
  }
}
```

### `403` `content_policy_violation`

The model provider refused the content. Error type: `content_policy_violation`. What to do: Change the content.

```json
{
  "error": {
    "code": 403,
    "message": "The request was blocked by the model provider's content policy.",
    "metadata": {
      "error_type": "content_policy_violation",
      "reason": "content_policy_violation"
    }
  }
}
```

### `404` `model_not_found`

The model is unknown, switched off, or not allowed for the key. Error type: `not_found`. What to do: Check the model id and the key's allowed models.

```json
{
  "error": {
    "code": 404,
    "message": "The model openai/gpt-5-nano does not exist or you do not have access to it.",
    "metadata": {
      "error_type": "not_found",
      "reason": "model_not_found",
      "model": "openai/gpt-5-nano"
    }
  }
}
```

### `404` `route_not_found`

Unknown path or method. Error type: `not_found`. What to do: Check the endpoint and method.

```json
{
  "error": {
    "code": 404,
    "message": "No such endpoint. Check the URL and method.",
    "metadata": {
      "error_type": "not_found",
      "reason": "route_not_found"
    }
  }
}
```

### `409` `request_in_progress`

An identical request is still running. Error type: `invalid_request`. What to do: Wait, then retry. You get the first request's result.

```json
{
  "error": {
    "code": 409,
    "message": "An identical request is already in progress.",
    "metadata": {
      "error_type": "invalid_request",
      "reason": "request_in_progress"
    }
  }
}
```

### `413` `payload_too_large`

The body is over 16 MiB. Error type: `payload_too_large`. What to do: Send less.

```json
{
  "error": {
    "code": 413,
    "message": "The request body is too large. Send less than 16 MiB.",
    "metadata": {
      "error_type": "payload_too_large",
      "reason": "payload_too_large"
    }
  }
}
```

### `429` `rate_limited`

The key's rate limit was reached. Error type: `rate_limit_exceeded`. What to do: Back off. Honour `Retry-After`.

```json
{
  "error": {
    "code": 429,
    "message": "Rate limit reached for this API key. Retry after 12 seconds.",
    "metadata": {
      "error_type": "rate_limit_exceeded",
      "reason": "rate_limited"
    }
  }
}
```

### `429` `concurrency_limited`

Your account has too many requests running at once. Error type: `rate_limit_exceeded`. What to do: Back off. Honour `Retry-After`.

```json
{
  "error": {
    "code": 429,
    "message": "Too many requests are running at once for this account. Wait for some to finish, then retry.",
    "metadata": {
      "error_type": "rate_limit_exceeded",
      "reason": "concurrency_limited"
    }
  }
}
```

### `429` `edge_rate_limited`

Too many requests from one IP address, or too many rejected keys from it. Error type: `rate_limit_exceeded`. What to do: Back off. Honour `Retry-After`.

```json
{
  "error": {
    "code": 429,
    "message": "Too many requests from this address. Retry after 30 seconds.",
    "metadata": {
      "error_type": "rate_limit_exceeded",
      "reason": "edge_rate_limited"
    }
  }
}
```

### `408` `upstream_timeout`

The model provider took too long. Error type: `timeout`. What to do: Retry with backoff.

```json
{
  "error": {
    "code": 408,
    "message": "The model provider took too long to respond.",
    "metadata": {
      "error_type": "timeout",
      "reason": "upstream_timeout"
    }
  }
}
```

### `500` `internal_error`

A problem on our side. Error type: `server`. What to do: Retry once. Contact support with the request id.

```json
{
  "error": {
    "code": 500,
    "message": "Something went wrong on our side. Reference: req_8f2c91.",
    "metadata": {
      "error_type": "server",
      "reason": "internal_error"
    }
  }
}
```

### `502` `provider_error`

The model provider returned an error. Error type: `provider_unavailable`. What to do: Retry with backoff.

```json
{
  "error": {
    "code": 502,
    "message": "The model provider returned an error.",
    "metadata": {
      "error_type": "provider_unavailable",
      "reason": "provider_error"
    }
  }
}
```

### `503` `service_unavailable`

The service is temporarily unavailable. No model was called. Error type: `server`. What to do: Retry with backoff.

```json
{
  "error": {
    "code": 503,
    "message": "The service is temporarily unavailable.",
    "metadata": {
      "error_type": "server",
      "reason": "service_unavailable"
    }
  }
}
```

### `529` `provider_overloaded`

The model provider is overloaded. Error type: `provider_overloaded`. What to do: Retry with backoff.

```json
{
  "error": {
    "code": 529,
    "message": "The model provider is overloaded. Retry shortly.",
    "metadata": {
      "error_type": "provider_overloaded",
      "reason": "provider_overloaded"
    }
  }
}
```

## Errors on `POST /v1/messages`

Anthropic's SDKs parse Anthropic's error body, so errors on `POST /v1/messages` (including authentication and `route_not_found`) use it. `error.type` is Anthropic's type, chosen from the HTTP status. `error.reason` is the specific cause, the same values as `metadata.reason` above, and per-reason extras such as `model` sit beside it inside `error`. The message text is the same. There is no `error.code` and no `metadata`. Status codes and headers don't change.

```json
{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "message": "You didn't provide an API key. Send it as `x-api-key: YOUR_KEY` or `Authorization: Bearer YOUR_KEY`.",
    "reason": "missing_api_key"
  }
}
```

| Status | `error.type` | Reasons |
|---|---|---|
| `400`, `409`, `413` | `invalid_request_error` | `invalid_body`, `key_not_in_header`, `context_length_exceeded`, `request_in_progress`, `payload_too_large` |
| `401` | `authentication_error` | `missing_api_key`, `invalid_api_key` |
| `402` | `billing_error` | `insufficient_credits`, `key_spend_cap_exceeded` |
| `403` | `permission_error` | `account_suspended`, `content_policy_violation` |
| `404` | `not_found_error` | `model_not_found`, `route_not_found` |
| `429` | `rate_limit_error` | `rate_limited`, `concurrency_limited`, `edge_rate_limited`. `Retry-After` is always sent. |
| `408`, `500`, `502`, `503` | `api_error` | `upstream_timeout`, `internal_error`, `provider_error`, `service_unavailable` |
| `529` | `overloaded_error` | `provider_overloaded`. `Retry-After` is always sent. |

After a stream has started, the status is already `200`, so the error is a final `event: error` event in the same format, and then the stream ends.

```text
event: error
data: {"type":"error","error":{"type":"overloaded_error","message":"The model provider is overloaded. Retry shortly.","reason":"provider_overloaded"}}
```

The `429` errors that come from Cloudflare's edge rules can't tell this path apart and keep the standard body above.
