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"
}
}
type is the broad class. code is the thing to branch on. param names the offending field where one exists.
Three errors do not use this envelope — model_capability_error, no_compatible_model and invalid_app_attribution carry a flatter, Stav-specific body where error may be a string or an object and there is no code field. Read the error key defensively.
Authentication and authorisation
| Code | Status | Meaning | Fix |
|---|---|---|---|
missing_api_key | 401 | No Authorization: Bearer and no x-api-key | Send the credential |
invalid_api_key | 401 | Wrong prefix, unknown key, or revoked | Check for sk_stav_live_ / sk_stav_test_; legacy keys no longer work |
Request validation
| Code | Status | Meaning |
|---|---|---|
invalid_value | 400 | A declared parameter carries an unacceptable value — e.g. an unrecognised reasoning_effort |
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 slug, or your team cannot see it. Deliberately ambiguous |
model_not_chat / model_not_chat_compatible |
Unknown request fields are also rejected with a 400 naming the field — see Request parameters.
Routing
| Code | Status | Meaning | Fix |
|---|---|---|---|
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 team | Check the @team/router spelling |
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
| Code | Status | Meaning |
|---|---|---|
rate_limit_exceeded | 429 | Three sources — see below. Carries Retry-After |
circuit_breaker_open | 503 | The provider is in a failure state and Stav has stopped sending to it |
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 code, not on the status |
Surface-specific: embedding_backend_error, rerank_backend_error and rerank_commercial_unsupported on /v1/embeddings and /v1/rerank.
The three sources of rate_limit_exceeded
- Stav's per-key limiter. Carries
Retry-Afterand theX-RateLimit-*headers. - Stav's admission queue — the inference queue is full or timed out. Carries
Retry-Afteronly. - 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"}}
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.
Retry guidance
| Situation | Retry? |
|---|---|
429 with Retry-After | Yes — honour the header, 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 |
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. On a successful response it is
X-Stav-Request-Id; on an error, headers are not attached, so quote the timestamp and the exact request you sent. - Look at
code, notmessage. Messages are written for humans and will change. - For routing failures, open Monitor → Routing in the Customer Portal and find the request. It shows every candidate that was considered and why each was dropped.
- For
unsupported_parameter, checkGET /v1/models/{id}—stav_capabilitiestells you what the model honours before you send anything.