CORE

API errors: codes, messages, and what to do

Every error is returned in the OpenAI envelope with an appropriate HTTP status - compatible with OpenAI SDK error handling. Below, one section per code: the literal message, the cause, the checks, and whether a retry is worth anything.

Error shape

An error response is always JSON. If the body is HTML, it did not come from our API but from a filter or proxy in between; that case is covered below.

JSON
{
  "error": {
    "message": "Insufficient credits.",
    "type": "insufficient_quota",
    "param": null,
    "code": "insufficient_quota"
  }
}

Branch on error.code rather than the status - it is more precise: one 402 covers both an empty balance and a key that hit its cap.

Every status at a glance

400invalid_request_errorMalformed request body or parameter.
400content_policy_violationContent rejected by moderation.
401authentication_errorThe key is missing or invalid.
402insufficient_quotaNot enough credits (insufficient_quota), or the key’s spending cap is exhausted (spend_limit_exceeded).
403permission_errorThe key is valid but the surface is outside its scopes (insufficient_scope).
404not_found_errorModel does not exist or is unavailable.
409invalid_request_errorA request with this Idempotency-Key is still in flight (idempotency_conflict).
429rate_limit_errorKey rate limit exceeded.
500api_errorAn internal gateway error (internal_error).
502api_errorModel-side error.
503service_unavailableThe model is temporarily unavailable.
504api_errorRequest exceeded the allowed time.

400 - invalid_request_error

The request body failed validation before any model saw it: a required field is missing, a value is out of range, or the requested mode is not available for that model. The response’s param field names the culprit.

What you see

`messages` is required and must be a non-empty array. (param: "messages")
`model` is required. (param: "model")
`prompt` is required. (param: "prompt")
Failed to process the reference image. (param: "image")
Image editing is not available for `<model>`. (param: "image")
Resolution 4k is not available for `<model>`. (param: "resolution")
Text-to-video is not available for `<model>` - a reference image is required.
Upstream rejected the request.
Content was rejected by the moderation system. (type: content_policy_violation)

What to check

  • Read error.param - it points at the exact field rather than "somewhere in the body".
  • Check that messages is a non-empty array and model is a catalog id.
  • For images and video, verify the model’s modes: image editing and image-to-video are not universal.
  • If type is content_policy_violation the shape is fine - moderation rejected the content, so rephrase the prompt.

Retry or not

NO RETRYThe same body returns the same answer. A retry only makes sense after the request is fixed.

401 - authentication_error

The key was not sent, does not exist, or has been revoked. Keys are stored as SHA-256 hashes, so an existing key cannot be looked up - only reissued.

What you see

Missing API key. (code: invalid_api_key)
Invalid API key provided. (code: invalid_api_key)

What to check

  • The header must be Authorization: Bearer ar-… (x-api-key is accepted too).
  • Check the key was not truncated on copy and carries no line breaks.
  • Make sure the key is not revoked in the dashboard: a revoked key answers with the same 401.
  • Check the base URL: https://api.altrouter.ai/v1 - an ar-… key will not work against another gateway.

Retry or not

NO RETRYNothing changes until the key or the header changes.

402 - insufficient_quota

There is not enough money to reserve the cost of the request - either on the organization balance, or within a particular key’s spending cap. Two different codes, two different fixes.

What you see

Insufficient credits. (code: insufficient_quota)
This key has reached its month spend limit. (code: spend_limit_exceeded)

What to check

  • Look at error.code: insufficient_quota is the balance, spend_limit_exceeded is the key’s cap.
  • For insufficient_quota, top up in the dashboard.
  • For spend_limit_exceeded the balance may be full: raise or remove the key’s cap. Windows are day, week, month and all-time.
  • Remember holds: an estimated cost is reserved up front, so available funds sit below the balance by the size of open holds.

Retry or not

NO RETRYOnly after a top-up, a cap change, or the window rolling over (day/week/month).

403 - permission_error

The key is genuine but is reaching for a surface outside its scopes. The second, rarer case: the client address is on the block list - such a request is refused before routing and before authentication.

What you see

API key lacks scope: videos (code: insufficient_scope)
Access denied. (code: access_denied)

What to check

  • Compare the key’s scopes in the dashboard with the surface you are calling: chat, images, videos.
  • A key with no scope list has access to everything; a scoped key only to what is listed.
  • If you need another surface, issue a new key with that scope rather than editing the request.
  • The access_denied code means an address-level block, not a key problem.

Retry or not

NO RETRYA retry will not help: you need a key with the right scope.

404 - not_found_error

The requested model is not in the catalog, or it belongs to another surface - a video model asked for at /v1/chat/completions, say. The same status answers an unknown path.

What you see

The model `gpt-5` does not exist or is not a chat model. (code: model_not_found)
The model `x` does not exist or is not an image model. (code: model_not_found)
Unknown endpoint. (code: unknown_endpoint)

What to check

  • Take the id verbatim from GET /v1/models or the catalog - ids are slugs like gpt-5.4 or gemini-2.5-flash.
  • Check the surface: chat models go to /v1/chat/completions, images to /v1/images/generations, video to /v1/videos.
  • For unknown_endpoint, check the path and the base URL: https://api.altrouter.ai/v1.

Retry or not

NO RETRYPointless to retry - change the model id or the path.

429 - rate_limit_error: rate limit exceeded

The key went past its requests per minute. The default is 600 rpm per key, though an individual key can carry its own. The window is fixed at 60 seconds and is counted per key, not per organization.

What you see

Rate limit exceeded. (code: rate_limit_exceeded, Retry-After: 60)
Too many sign-in emails requested. Try again later.

What to check

  • Read the Retry-After header - it carries the seconds left in the window.
  • Watch x-ratelimit-limit and x-ratelimit-remaining on every response: the headroom is visible before you run out.
  • Spread peak load across several keys, or ask for a higher rpm on the key.
  • Do not add parallelism in response to a 429 - it only burns the window faster.

Retry or not

RETRYYes - after Retry-After seconds (usually 60). A provider’s own 429 never reaches you: the router moves that request to a failover route.

500, 502, 503, 504 - failures on our side and the provider’s

500 is an internal gateway error; its text is always the same and the detail stays in our logs rather than going to the client. 502 means the provider answered with an error. 503 means no healthy route was found for the model - the router had already walked its failover providers before returning it. 504 means the request did not finish inside its deadline.

What you see

Internal server error. (500, code: internal_error)
The upstream provider returned an error. (502, code: upstream_error)
Upstream authentication failed. (502, code: upstream_error)
No healthy provider is available for this model. (503, code: no_provider_available)
All providers failed (last: kie 500). (503, code: no_provider_available)
The request exceeded the allowed time. (504, code: timeout)

What to check

  • Retry with exponential backoff - this is the one category where a retry genuinely changes the outcome.
  • If a 503 persists, try another model: the status is about that model’s routes, not about the whole gateway.
  • On a 504, shorten the expected answer (max_tokens) or switch to streaming: SSE delivers the first tokens without waiting for the end.
  • For /v1/images and /v1/videos, retry with an Idempotency-Key header so you do not create a second generation and a second charge.

Retry or not

RETRYYes, with exponential backoff: 1, 2, 4, 8 seconds and a capped attempt count. An open hold is released automatically, so a failed request does not eat the balance.

Which error codes are safe to retry

The short answer: retry 429 and the 5xx family. The 4xx family describes the state of your request, key or account - and that state will not change on its own.

400NOFix the body - error.param names the field.
401NOReplace the key or the header.
402NOTop up, or raise the key’s cap.
403NOYou need a key with the right scope.
404NOCheck the model id and the path.
409NOWait for the first request with that Idempotency-Key.
429YESAfter Retry-After seconds (the window is 60 seconds).
500YESExponential backoff: 1, 2, 4, 8 seconds.
502YESSame: the provider may have recovered.
503YESSame, but switch model if a 503 persists.
504YESRetry, plus a shorter answer or streaming.
499NOThe client dropped the connection - nothing failed.
!
When retrying /v1/images and /v1/videos, send an Idempotency-Key header: a repeat call with the same key replays the first result, and a concurrent attempt gets a 409 idempotency_conflict instead of a second generation and a second charge.

Forbidden: access denied by security policy - and other errors that are not ours

Some of the messages people bring to support are not returned by our API at all. They are easy to tell apart: the body is an HTML page rather than JSON, with no error.type and no error.code in it.

Forbidden: access denied by security policy.
Sorry, you have been blocked. You are unable to access openrouter.ai

That is a site security filter (a WAF, most often Cloudflare) refusing a connection by client address or country. The request never reaches an API, so the key, the balance and the body make no difference, and a retry changes nothing. There are two ways out: a different network route, or a gateway that talks to the models itself. altrouter requests go to api.altrouter.ai and we are the ones calling OpenAI, Anthropic and Google.

i
Our own 403 looks different: JSON with type: "permission_error" and code insufficient_scope - the key is missing a scope.

Codes you only see in the logs

A 499 status with code client_closed is not a service failure: it marks a request whose connection the client itself dropped - a cancelled stream, for instance. The code stream_error marks a stream that broke after it started: the 200 headers had already gone out, so the failure has no HTTP status of its own and shows up in the request log instead.

Frequently asked questions

What does "Forbidden: access denied by security policy" mean?
That is a site security filter (WAF) answering, not a model API: it arrives as an HTML page with no JSON envelope and no error.type field. Filters return it when the request comes from an address or a country they do not admit. Neither a new key nor a retry helps: you need a different network route, or a gateway that talks to the models on your behalf. Every altrouter error comes back as JSON in the OpenAI envelope, and a 403 there means one thing only: the key is missing the required scope.
What should I do about "Sorry, you have been blocked. You are unable to access openrouter.ai"?
That is a Cloudflare block page served by the site itself - the request never reaches an API. The body is HTML with a Ray ID rather than JSON, which is why an OpenAI SDK reports garbage instead of a model error. Your key, balance and request body are irrelevant: the connection was cut by client address. A different network route, or a gateway with its own entry point, is what resolves it - altrouter requests go to api.altrouter.ai and we talk to the vendors.
Which error codes are safe to retry?
Retry 429, 500, 502, 503 and 504: those are transient - a rate limit, an internal gateway failure, a provider error, no healthy route, and a timeout. Retrying 400, 401, 402, 403, 404 or 409 is pointless: until the body, the key, the balance or the scope changes, the answer will be identical. On a 429 wait the number of seconds in the Retry-After header; on 5xx back off exponentially (1, 2, 4, 8 seconds) with a capped number of attempts. For /v1/images and /v1/videos send an Idempotency-Key header so a retry cannot create a second generation and a second charge.
What does a 400 from the router mean?
400 is invalid_request_error: the request body failed validation before any model saw it. The param field names the culprit - for example "`messages` is required and must be a non-empty array." with param "messages", or "`model` is required." with param "model". One special case: a 400 whose type is content_policy_violation means the request did reach the model and moderation rejected the content. Retrying a 400 changes nothing until the request is fixed.
What does error 402 mean and how do I clear it?
402 carries the type insufficient_quota and comes in two shapes. The first is "Insufficient credits." with code insufficient_quota: the organization balance cannot cover the reservation for this request, so top up in the dashboard. The second is "This key has reached its month spend limit." with code spend_limit_exceeded: the key itself hit its day, week, month or all-time cap. In that second case the balance can be full - raise or remove the key cap, or wait for the next window.
Error 429: rate limit exceeded - what now?
429 rate_limit_error means the key went past its requests per minute: 600 rpm by default, though an individual key can carry its own limit. The response includes a Retry-After header with the seconds left in the window, plus x-ratelimit-limit and x-ratelimit-remaining so you can see the headroom. Wait out Retry-After and repeat the request; adding parallelism on top of a limit does not help. A 429 from a provider never reaches you - the router moves that request to a failover route.
What does "invalid credits amount" mean?
altrouter never returns that text: running out of money here is an HTTP 402 with error.code insufficient_quota and the message "Insufficient credits.". "invalid credits amount" comes from a different service and refers to a top-up amount rather than a model request - check the amount in that service’s payment form. An altrouter balance is topped up in the dashboard, and no credit amount is ever sent in an API request body.
Why did the model return no text?
An empty answer is usually not an HTTP error: the request returns 200, and the reason shows up in choices[0].finish_reason and usage.completion_tokens. If the answer stops mid-sentence, look at max_tokens: when the parameter is omitted, 4096 is substituted, and a long generation hits that ceiling. If the content was rejected you get an actual error instead - a 400 with type content_policy_violation. A stream that breaks after it started is logged in the dashboard with code stream_error: the HTTP status had already gone out as 200.
NextLimits & credits →