On this page
Errors
The error response format, and every error the API returns with its status, reason, an example and what to do.
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.
{
"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-idis on every response. Include it when you contact support.WWW-Authenticate: Bearer realm="router.africa"is on every401.Retry-After(seconds) is always sent on429,503and529. If no other value applies it is 5 seconds. See Rate limits.x-idempotent-replay: trueis sent only when the response is a stored replay of an earlier identical request. You aren’t charged again. See Idempotency and retries.- Rate-limit headers such as
x-ratelimit-remainingare not sent.
Handle an error
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_8f2c91import os
import openai
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"))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.
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.
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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
{
"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.
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.