Fejl
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 does not pass the policy wall for this request. The message says which check refused it: the endpoint or your node is offline, its Inference Provider is not serving, it has no price (or none in your workspace's currency), Stav Tunnel is off for your workspace, the Router ignores your own nodes, or a workspace policy rule — compute country or region, zero data retention, no training, sovereignty floor, a blocked provider | Act on the check the message names, 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 catalogue model has that slug, alias or bare name. A private |
model_not_published | 404 | The slug names a model that is on Stav but not published — a draft, coming soon or retired — or a draft served only by a Stav Tunnel node outside your workspace. The message says which; it never names another workspace |
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 region ( |
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. Every listed format can be read, so the file is likely cut short or damaged. Re-encode as wav or mp3 |
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 |
|
rerank_not_enabled | 404 |
|
rerank_not_supported | 501 | The model's Inference Provider speaks a dialect with no |
rerank_input_too_long | 400 | The query, or the query with one document, is longer than every Provider Endpoint of the model reads. |
embeddings_not_supported | 501 | The endpoint that serves the model is a Stav Tunnel node, and no node runtime serves |
response_not_found | 404 |
|
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 | |
case_no_eligible_endpoint | 400 | A case Router picked a case, and neither the case's target nor its fallback has a Provider Endpoint that meets the case's rules. The message names the case. The default case is not used, because it may run under looser rules |
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 |
node_offline | 502 | The model runs on your own Stav Tunnel node, and the node is not connected. The message names the node |
node_timeout | 504 | Your node did not answer in time |
node_busy | 503 | Your node's local model server answered 429, 413 or a 5xx, or is not running. Stav tries the model's other endpoints first; you see this only when none could serve |
node_backend | 502 | Your node's local model server refused the request itself (for example a 400). Not retried elsewhere — the request fails everywhere |
/v1/embeddings answers with the same codes: a provider 429 on every endpoint the model has is rate_limit_exceeded with Retry-After, a 5xx is provider_error with the status passed through, a timeout backend_timeout.
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), byok_upstream_error (502) and byok_key_unusable (409 — your key cannot serve the endpoint the request was routed to, for example one in another provider region, and the key is set to strict or your wallet cannot pay for Stav's pooled key instead).
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 |
503 node_busy | Yes, with backoff — your own machine was full or busy |
502 node_offline | Yes, once the node is connected again |
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.