Feil
The error envelope, the full code catalogue, and what each one actually means — including the ones unique to routing and sovereignty.
The envelope
Most /v1 errors use the OpenAI error shape:
{
"error": {
"message": "human-readable, actionable",
"type": "invalid_request_error",
"param": "reasoning_effort",
"code": "invalid_value",
"request_id": "chatcmpl-8f3a1c…",
"docs_url": "https://stav.ai/docs/api/errors#request-validation"
}
}
type is the broad class. code is the thing to branch on. param names the offending field where one exists. request_id is the identifier to quote — the same value as the X-Stav-Request-Id response header, which every error carries too. docs_url opens the section of this page that explains the code; when an error has no code (a malformed request body, for instance) it points at the page itself.
/v1/messages keeps the Anthropic shape — {"type": "error", "error": {"type": …, "message": …}} — with request_id and docs_url inside error. Anthropic-shaped errors have no code.
One error does not use either envelope. model_capability_error carries a flat, Stav-specific body — error is the string "model_capability_error", beside capability, suggestion, supported_models, and (since 2026-09-16) code, request_id and docs_url at the top level. Read the error key defensively.
Every code on this page is declared in one registry inside the gateway, and a build check fails when the two disagree. A code you receive that is not listed here is a documentation bug — tell us, quoting the request_id.
Authentication and authorisation
| Kode | Status | Betydning | Løsning |
|---|---|---|---|
missing_api_key | 401 | No | Send the credential |
invalid_api_key | 401 | Wrong prefix, unknown key, or revoked | Stav keys start with |
expired_api_key | 401 | Past expiry, or a rotated key past its grace window | Use the current key |
insufficient_scope | 403 | Valid key, wrong scope for this endpoint | Add the scope, or use a key that has it |
model_not_permitted | 403 | Valid key; your workspace's policy excludes that model | Change the workspace policy, or use a model your workspace allows |
router_not_allowed_for_key | 403 | The key is pinned to a different router, or the router is not in its allowlist | Use the key's permitted router |
router_required_for_key | 403 | The key is restricted to a Router, and the request named a model or called | Send |
ip_not_allowed | 403 | The key has an IP allowlist and the caller's address is not on it | Call from an allowed address, or edit the key's allowlist |
model_below_sovereignty_floor | 403 | The pinned model sits below the workspace's sovereignty floor | Pin a model at or above the floor, or route with |
endpoint_not_admissible | 403 | The pinned endpoint fails the workspace's policy wall — compute region, ZDR or no-training | Pin a different endpoint, or let the Smart Router choose |
provider_blocked | 403 | The named model or pinned endpoint is served only by an Inference Provider your workspace blocked | Pick a model another Inference Provider serves, or unblock the provider under Govern → Key policies → Reach |
model_blocked_by_key_policy | 403 | The request came from a personal API key, and your workspace's Personal-Keys policy blocks the model or allows a list it is not on. On | Use a model the policy allows, or ask a workspace owner or admin to change it under Govern → Key policies → Personal keys |
store_not_permitted | 403 |
| Omit |
visitor_cannot_route | 403 | A visitor (Playground) credential asked for a router or | Sign in, or pin a free endpoint |
Billing and spend
| Kode | Status | Betydning | Løsning |
|---|---|---|---|
wallet_empty | 402 | The workspace's prepaid wallet cannot cover the request, or the BYOK fee. | Buy credits at |
budget_exceeded | 402 | A hard spend cap on the workspace, the API key or the router was reached; the message names which | Raise the cap, or wait for the next day/month |
request_cost_ceiling | 402 | This one request's projected cost exceeds the per-request ceiling | Lower |
gate_unavailable | 503 | The wallet gate could not decide because a read failed. Nothing was charged. | Retry with backoff |
tariff_unavailable | 503 | The endpoint has no open sell tariff in your workspace's currency, so the request could not be charged. Ours to fix | Retry once; if it persists, use another endpoint and tell us |
Request validation
| Kode | Status | Betydning |
|---|---|---|
invalid_value | 400 | A declared parameter carries an unacceptable value — e.g. an unrecognised |
unsupported_parameter | 400 | The parameter cannot be honoured by the model that resolved. The message names the parameter, the model, and which providers would accept it |
model_not_found | 404 | No such model id, or your workspace cannot see it. Deliberately ambiguous |
model_not_chat / model_not_chat_compatible | 400 | The model is an embedding model, or is not usable on the chat surface |
structured_output_not_supported | 501 | The model's provider cannot enforce a schema |
structured_output_invalid | 400 | The schema itself was rejected |
model_capability_error | 400 | The model lacks a capability the request needs ( |
capability_unreachable | 400 | The model supports the capability, but not with the parameters you sent on any API surface this endpoint can reach — for example tools on a model that takes them on chat completions only with |
endpoint_not_found | 404 | The model has no endpoint with the pinned provider or tier ( |
audio_not_enabled | 404 | The audio surface you called ( |
audio_too_large | 413 | The uploaded file is over the serving endpoint's upload cap. The message names the limit in MB and the model |
audio_too_long | 400 | The audio is longer than every serving endpoint of the model takes. The message names the limit and the model. Split the file and send the parts |
audio_duration_unknown | 400 | The duration could not be read from the file and the endpoint is priced per minute, so the request cannot be sized before it runs. Re-encode as wav, mp3, m4a, ogg or flac |
transcription_not_supported | 501 | The model's Inference Provider speaks a dialect with no |
speech_not_supported | 501 | The model's Inference Provider speaks a dialect with no |
speech_input_too_long | 413 | The |
speech_format_not_supported | 400 | The |
decisions_not_enabled | 404 |
|
image_input_not_supported | 400 | Images were sent to |
decisions_not_supported | 501 | The model's Inference Provider has no decisions door |
decision_refused | 422 |
|
upstream_invalid_answer | 502 |
|
embeddings_not_supported | 501 | The endpoint that serves the model is a Stav Tunnel node, and no node runtime serves |
response_not_found | 404 |
|
inference_tier_removed | 400 | The model slug carried a retired |
endpoint_sunset | 400 | The pinned endpoint has been retired by its provider |
service_tier_unpriced | 400 | The |
modality_unpriced | 400 | The request carries an input (audio, image, video) the model prices separately and Stav cannot yet measure on that endpoint. The message names it. Send text only, or use a model that prices every input the same |
hint_conflict | 400 | Reserved for routing hints that are not active yet. Not returned today |
hint_override_locked | 400 | Reserved for routing hints that are not active yet. Not returned today |
hint_no_eligible_model | 400 | Reserved for routing hints that are not active yet. Not returned today |
On /v1/chat/completions, unknown request fields are also rejected with a 400 naming the field — see Request parameters.
Routing
| Kode | Status | Betydning | Løsning |
|---|---|---|---|
policy_unavailable | 503 | Your workspace's routing policy (sovereignty floor, region, ZDR, no-training) could not be read, so the request was not served rather than served without it. Nothing was charged. | Retry with backoff |
no_eligible_models | 404 | Every candidate was filtered out — by the sovereignty floor, an allowlist, a capability requirement, or endpoint health | Relax one constraint. Check the floor first |
quality_floor_unsatisfied | 400 | The quality floor removed every candidate. Unrated models are held out whenever a floor is set | Lower the floor, or get the model rated |
router_not_found | 404 | The named router does not exist, is archived, or belongs to another workspace | Check the |
router_not_supported | 400 |
| |
invalid_router_ref | 400 | The router reference is malformed. The grammar is exactly |
no_eligible_models is the one people hit most, and it is almost always two constraints interacting rather than one being wrong — a floor and a capability requirement that have no model in common, most often. Widen one and retry.
Upstream and capacity
| Kode | Status | Betydning |
|---|---|---|
rate_limit_exceeded | 429 | Two sources — see below |
circuit_breaker_open | 503 | The provider is in a failure state and Stav has stopped sending to it |
provider_account_suspended | 503 | The provider has suspended the account serving this endpoint. Not retry-worthy until we say so |
provider_unavailable / model_unavailable | 503 | No provider, or no serving endpoint, could take the request right now |
timeout | 504 | On |
upstream_error | passthrough | On |
provider_error | passthrough | The upstream provider returned an error Stav could not resolve by failing over. The provider's own status is passed through — a provider 503 arrives as 503, a provider 402 as 402. Branch on |
backend_timeout | 504 | Connect, read, write or pool timeout that failover could not resolve |
backend_error | 502 | A non-HTTP transport failure against the provider |
internal_error | 500 | Ours. Quote the |
Surface-specific: embedding_backend_error on /v1/embeddings (the provider's status is passed through).
BYOK-specific, when the request ran through your workspace's own provider key: byok_key_invalid (502 — the provider rejected your key), byok_key_exhausted (429 — your provider account hit its limit or quota) and byok_upstream_error (502).
The two sources of rate_limit_exceeded
- Stav's per-key limiter. Carries
Retry-Afterand theX-RateLimit-*headers. - An upstream provider 429 passed through. A provider 429 is failover-eligible, so on a routed request Stav normally tries your next-best candidate instead. It reaches you when there was no candidate left — which includes every pinned model, and any request whose routing decision was replayed from cache and therefore carries no alternatives.
Errors on a streaming response
Once a stream is open, the HTTP status is already 200. Late errors arrive as an SSE frame:
data: {"error": {"message": "...", "type": "invalid_request_error", "code": "unsupported_parameter", "request_id": "chatcmpl-…", "docs_url": "https://stav.ai/docs/api/errors#request-validation"}}
data: {"id":"chatcmpl-…","choices":[{"index":0,"delta":{},"finish_reason":"error"}]}
data: [DONE]
Streaming clients must check each frame for an error key. Treating 200 as success is not sufficient on any streaming API, and it is not sufficient here. A late error that came from the provider carries the provider's own code — or the bare HTTP status when it sent none — so branch on the registry codes above first and treat anything else as provider_error.
Retry guidance
| Situation | Retry? |
|---|---|
429 rate_limit_exceeded | Yes — honour Retry-After when present, exponential backoff after |
503 circuit_breaker_open | Yes, with backoff. Prefer routing (model="auto") so Stav can pick a healthy provider for you |
504 backend_timeout | Yes, with backoff — the most retry-worthy failure there is |
502 backend_error | Once |
provider_error | Once. If it persists, the provider is genuinely down |
501 structured_output_not_supported | No. Use a model whose provider can enforce a schema |
500 internal_error | Once, then report with the request_id |
503 gate_unavailable | Yes, with backoff. Nothing was charged |
503 policy_unavailable | Yes, with backoff. Nothing was charged |
429 byok_key_exhausted | Yes, with backoff — it is your own provider account's limit |
Any 400 | No. The request is wrong; retrying produces the same error |
401 / 403 | No. Fix the credential or the policy |
404 no_eligible_models | No. Fix the constraints |
The single most effective reliability measure is to route rather than pin: failover across your allowed pool happens inside one request, before your retry logic ever sees a failure.
Debugging checklist
- Note the identifier. Every response, success or error, carries
X-Stav-Request-Id; error bodies repeat it asrequest_id. Quote it — it is the same id you will find under Your workspace → Monitor → Request log, where a failed request shows its code linked back to this page. - Look at
code, notmessage. Messages are written for humans and will change.docs_urltakes you to the section for the code. - For routing failures, open the request in Your workspace → Monitor → Request log. Its routing detail shows how the model was chosen.
- For
unsupported_parameter, checkGET /v1/params(which dialects honour each parameter) andGET /v1/models/{id}(stav_capabilities) before you send anything.