Every code the API can return, which of them are worth retrying, and the ceilings you will hit first. Read from the handler source on 2026-08-20, not written from memory.
Every error uses OpenAI's shape, plus a quorum block carrying the
request id where one exists — so a failure is as traceable as a success.
{
"error": {
"message": "Rate limit exceeded for this API key.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"param": null
},
"quorum": { "request_id": null }
}
Branch on code, never on message.
Codes are stable; messages are prose and may be reworded.
The complete set, read from the handlers on 2026-08-20. If you get something not on this list, that is a bug on our side — tell us.
| Code | HTTP | Retry? | What it means |
|---|---|---|---|
invalid_api_key | 401 | No | Missing, malformed, or not a real key. Check the Bearer prefix. |
revoked_api_key | 401 | No | The key expired or was revoked. Issue a new one. |
org_suspended | 403 | No | The organisation's access is suspended. Usually billing. |
browser_origin_not_allowed | 403 | No | The request carried a browser Origin header. See below — this one catches people out. |
insufficient_scope | 403 | No | The key exists but lacks the scope for this endpoint. |
| Code | HTTP | Retry? | What it means |
|---|---|---|---|
invalid_model | 400 | No | model missing or not a string. |
invalid_messages | 400 | No | messages missing or empty. |
model_not_found | 404 | No | No mode with that id is exposed to your organisation. Call GET /v1/models. |
stream_not_supported | 400 | No | You sent stream: true. See below. |
context_length_exceeded | 413 | No | Prompt exceeds the key's max_input_tokens — 8,000 unless raised. |
request_in_progress | 409 | Wait | A call with this Idempotency-Key is still running. Wait for it; do not fire another. |
missing_request_id | 400 | No | Receipts need a request id. |
receipt_not_found | 404 | No | No receipt under your organisation for that id. Another org's id returns this, never a partial leak. |
method_not_allowed | 405 | No | Wrong verb for the path. |
| Code | HTTP | What it means |
|---|---|---|
invalid_buckets | 400 | buckets missing or empty. |
invalid_bucket | 400 | Unknown bucket name. Valid: light, medium, hard. |
invalid_bucket_count | 400 | Each bucket takes an integer from 1 to 100. |
too_many_questions | 400 | 300 questions is the cap for a single run. |
invalid_judges | 400 | Judges must be a subset of nova_pro, mistral_large, llama4_maverick. |
| Code | HTTP | Retry? | What it means |
|---|---|---|---|
rate_limit_exceeded | 429 | Yes | Requests-per-minute ceiling for this key. Retry-After is sent. |
concurrency_limit_exceeded | 429 | Yes | Too many calls in flight for the organisation. Retry-After is sent. |
service_unavailable | 503 | Yes | A dependency was unreachable and the gate failed closed rather than letting the call through unchecked. |
internal_error | 500 | Yes | Our fault. Safe to retry with the same Idempotency-Key. |
| Ceiling | Default | Scope |
|---|---|---|
| Requests per minute | 60 | Per API key |
| Concurrent requests | 5 | Per organisation |
| Input tokens | 8,000 | Per key, per request |
These are the defaults; a key can be raised. Both 429s carry
Retry-After in seconds — honour it and back off exponentially
rather than hammering.
Concurrency is the one that bites first in practice, because a deliberation holds its slot for tens of seconds. Five concurrent calls at a p50 of 28.5 s is roughly ten a minute sustained — well under the 60/minute rate ceiling. If you are fanning out, queue rather than parallelise.
/v1/estimate bills nothing but takes the same key and the same
ceilings as a billed call. Real cost per estimate is about $0.00006 — tiny,
but tiny multiplied by unlimited automated volume is not tiny.
Send an Idempotency-Key header on /v1/chat/completions
for anything a retry could duplicate. Behaviour:
quorum.replayed: true. Not re-run, not re-billed.409 request_in_progress.
Wait; a second call will not overtake the first.If you omit the header, one is derived from your organisation, key, model and messages — so an identical repeat is deduplicated whether you asked for it or not. Vary the messages, or send your own key, when you genuinely want a second opinion on the same question.
Origin header is a 403Not a CORS misconfiguration on our side — a deliberate rule. This API is
server-to-server, and a real key presented from a browser is a leaked key. If
you are seeing this from a proxy or a test harness, strip the
Origin header.
stream: true is a 400, not a streamProvider token streaming is not implemented. It used to be silently ignored, which meant an SDK waiting for SSE got a plain JSON body and broke with no useful error. Failing loudly is the better wrong answer.
finish_reason: "cost_cap" is not an errorThe run hit the mode's cost ceiling. You get a real answer, from a shorter deliberation than the mode would otherwise have run. Treat it as a signal worth logging, not a failure worth retrying — a retry will hit the same ceiling.