API v1Updated 2026-09-21
Error Handling
The complete error-code table, the two native error envelope formats, and safe retry guidance.
Errors are returned as JSON with a stable code you can branch on. The envelope format depends on the endpoint: OpenAI-compatible endpoints use one shape, the Anthropic-compatible endpoint uses another.
Error formats#
/v1/chat/completions and /v1/responses return the OpenAI-shaped envelope:
OpenAI-shaped error (chat completions & responses)
{
"error": {
"message": "Insufficient balance. Top up PAYG credit or buy a Token Plan for this model.",
"type": "billing_error",
"code": "insufficient_balance",
"request_id": "req_..."
}
}/v1/messages returns the Anthropic-shaped envelope:
Anthropic-shaped error (messages)
{
"type": "error",
"error": {
"type": "billing_error",
"code": "insufficient_balance",
"message": "Insufficient balance. Top up PAYG credit or buy a Token Plan for this model."
},
"request_id": "req_..."
}Error codes#
| Status | Code | Type | Meaning |
|---|---|---|---|
| 400 | invalid_request | invalid_request_error | Malformed or invalid request body or parameters. Also returned when the upstream rejects a request with 400. |
| 401 | invalid_api_key | authentication_error | Missing key, wrong key, two headers with different values, or a blocked key. The response is identical in all cases. |
| 402 | insufficient_balance | billing_error | Your Token Plan and PAYG balance cannot cover the request. Top up before retrying. |
| 404 | model_not_found | not_found_error | The model identifier does not exist or is hidden. |
| 413 | request_too_large | invalid_request | The request body exceeds the size limit. |
| 429 | technical_limit | rate_limit_error | Passthrough of an upstream 429 — not a zrouter rate limit. zrouter applies no per-key limit; this signals the upstream is rate-limited. |
| 500 | internal_error | internal_error | An unexpected internal error. |
| 502 | upstream_error | upstream_error | The upstream provider failed to process the request. |
| 503 | overloaded | server_error / overloaded_error | Capacity limit reached (in-flight shed). Retry later. Type is server_error on OpenAI endpoints and overloaded_error on /v1/messages. |
| 503 | circuit_open | upstream_error | Circuit breaker open — upstream temporarily unavailable. A Retry-After header is set. |
Retry best practices#
Transient failures — 429, 5xx, 502, 503 — are safe to retry. Follow these rules to avoid double-charging or amplifying load:
- Exponential backoff with jitter on
429,5xx,502, and503— never a tight fixed-interval loop. - Retry only before the first byte. Once a stream has started, do not retry mid-stream.
- Never retry
402. Top up your balance first; retrying will not help. - Cap retries at 2–3 attempts for a persistent outage.
- Non-streaming timeout is 120s. Set your client timeout slightly below it so you see a clean error.
- Settlement is idempotent and debit-after. You are charged only after inference succeeds — failed requests are never billed, so a safe retry costs nothing until it succeeds.
Handling errors in code#
Branch on the stable code field — never parse the human-readable message. Both SDK families expose the HTTP status and the error code, so one pattern covers every endpoint:
import random
import time
from openai import OpenAI, APIStatusError
client = OpenAI(
api_key="YOUR_API_KEY", # zr_... key from the dashboard
base_url="https://api.zrouter.dev/v1",
)
RETRYABLE = {429, 500, 502, 503}
def chat(prompt: str, model: str = "<public-name>") -> str:
for attempt in range(3):
try:
resp = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content or ""
except APIStatusError as err:
code = err.code or "" # e.g. "insufficient_balance"
if err.status_code == 402 and code == "insufficient_balance":
raise # never retry: top up PAYG or buy a Token Plan first
if err.status_code in RETRYABLE and attempt < 2:
# exponential backoff with jitter, capped at 3 attempts
time.sleep(min(2 ** attempt, 8) + random.random())
continue
raise
raise RuntimeError("unreachable")