Skip to content

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  }}
FieldDescription
typeBroad class, such as authentication_error, invalid_request_error or upstream_error.
codeThe specific reason, stable enough to branch on.
messageA sentence for a developer reading logs. Don't parse it.
request_idMatches the id in Activity and in successful responses.
retry_after_msPresent on 429 and some 503s. Wait at least this long.

Status codes

StatusDescription
400The body is malformed, or a parameter is out of range for the model.
401Missing, malformed or revoked key.
402The credit balance is empty, or a spend limit was reached.
403The key isn't allowed to call this model or endpoint, or the account or network is suspended.
404Unknown model id or request id.
408The endpoint didn't respond in time.
413The prompt is larger than the model's context window.
429Your requests-per-minute limit, your plan's 5-hour or weekly allowance, or the endpoint's limit, was hit.
500A fault inside Odyssey. Safe to retry.
502 / 503Every allowed endpoint failed or refused the request.

What to retry

  • Retry on 429, 408 and 5xx, with exponential backoff and jitter.
  • Don't retry 400, 401, 403, 404 or 413. The same request will fail again.
  • Handle 402 as 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.