API reference
Errors
Status codes, error bodies and what to retry.
Errors use standard HTTP status codes and a single body shape, whether they come from Odyssey or from the model itself. Every error carries the request id, so a failure in your logs can be found in the dashboard.
Error shape
{ "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Rate limit of 30 requests per minute reached for your plan.", "param": null, "request_id": "req_5d18ba90c47e2f6183ac5b71", "retry_after_ms": 12000 }}| Field | Description |
|---|---|
type | Broad class, such as authentication_error, invalid_request_error or upstream_error. |
code | The specific reason, stable enough to branch on. |
message | A sentence for a developer reading logs. Don't parse it. |
request_id | Matches the id in Activity and in successful responses. |
retry_after_ms | Present on 429 and some 503s. Wait at least this long. |
Status codes
| Status | Description |
|---|---|
400 | The body is malformed, or a parameter is out of range for the model. |
401 | Missing, malformed or revoked key. |
402 | The credit balance is empty, or a spend limit was reached. |
403 | The key isn't allowed to call this model or endpoint, or the account or network is suspended. |
404 | Unknown model id or request id. |
408 | The endpoint didn't respond in time. |
413 | The prompt is larger than the model's context window. |
429 | Your requests-per-minute limit, your plan's 5-hour or weekly allowance, or the endpoint's limit, was hit. |
500 | A fault inside Odyssey. Safe to retry. |
502 / 503 | Every allowed endpoint failed or refused the request. |
What to retry
- Retry on
429,408and5xx, with exponential backoff and jitter. - Don't retry
400,401,403,404or413. The same request will fail again. - Handle
402as a signal, not a fault. Add credits or raise the limit; retrying without doing so just fails again.
async function withRetries(send, attempts = 3) { for (let attempt = 0; attempt < attempts; attempt++) { try { return await send(); } catch (error) { const status = error?.status; const retryable = status === 429 || (status >= 500 && status < 600); if (!retryable || attempt === attempts - 1) throw error; const wait = error?.headers?.["retry-after-ms"] ?? 2 ** attempt * 500; await new Promise((resolve) => setTimeout(resolve, Number(wait) + Math.random() * 250)); } }}Odyssey doesn't retry for you
A 502 or 429 reaches you as it happened, so retry with backoff in your own client. The official OpenAI and Anthropic SDKs already do this for these statuses.
Rate limits
Your account's requests per minute surfaces as 429 with rate_limit_exceeded. A plan whose 5-hour or weekly allowance is used up returns 429 with session_limit_reached or weekly_limit_reached, and retry_after_ms is the time until it resets — hours, not seconds, so don't retry in a loop. When the model itself is overloaded, you get 429 with upstream_rate_limited instead, and when the model is at capacity, 503 with model_busy. All of them carry retry_after_ms.
Responses include x-ratelimit-limit-requests and x-ratelimit-remaining-requests whenever a limit applies, and a retry-after header on 429. Watching the remaining count is cheaper than backing off after the fact.
Tracing a failure
Paste a request id into Activity to see the status it returned, where the time went and what the request cost. Failed requests are kept in the log too.