Predictable failures.
The API returns standard HTTP status codes with a JSON error body shaped for the protocol you called — Anthropic-shaped on /v1/messages, OpenAI-shaped on the OpenAI endpoints. Match on the status; read error.message for the human explanation.
Status codes
| Status | Type | Meaning |
|---|---|---|
400 | invalid_request_error | Malformed JSON or bad parameters — fix the request; retrying the same body will fail the same way. |
401 | authentication_error | Missing or invalid key. Check the header for the protocol you're calling. |
403 | permission_error | The key is valid but not allowed to do this. |
404 | not_found | Unknown route or unknown model id — check the path and the catalog (GET /v1/models). |
413 | invalid_request_error | Request body too large. Trim the payload or split the work. |
422 | invalid_request_error | The request declares a requirement the chosen model cannot guarantee (e.g. a capability the model doesn't have). Switch models or drop the requirement. |
429 | rate_limit_error | Transient rate limit (requests/min, tokens/min or concurrency). Carries retry-after (seconds) — back off and retry. |
429 | allowance_exhausted | Monthly included allowance used up — retrying won't help until the month rolls over. The message names your plan and the next tier; retry-after is the seconds to reset. Upgrade to lift it immediately. |
5xx | api_error | The model did not return a response. Please retry — transient by design; a retry with backoff almost always succeeds. |
Error body shape
Every error returns a JSON body with an error object carrying a type and a message. On the Anthropic endpoint it is Anthropic-shaped:
{
"type": "error",
"error": {
"type": "authentication_error",
"message": "Invalid API key."
}
}On the OpenAI endpoints it is OpenAI-shaped:
{
"error": {
"type": "invalid_request_error",
"message": "Invalid API key."
}
}Retries and backoff
The retry recipe every production agent should ship:
- Retry only what can succeed —
429with typerate_limit_errorand5xx. A 429 with typeallowance_exhaustedwon't clear until the month resets (or you upgrade), and a4xxwill fail identically on every attempt; fix the request instead. - Honor
retry-afterexactly — on429it is the number of seconds until capacity opens. Waiting less just burns attempts. - Exponential backoff with jitter everywhere else — e.g. 1s, 2s, 4s, 8s with ±20% jitter, capped around five attempts.
- Fail loud after the cap — surface the final
error.message; it is written to be actionable.
import random, time
def with_retries(send, max_attempts=5):
for attempt in range(max_attempts):
r = send()
if r.status_code < 400:
return r
if r.status_code == 429:
time.sleep(int(r.headers.get("retry-after", "1")))
elif r.status_code >= 500:
time.sleep((2 ** attempt) * (1 + random.uniform(-0.2, 0.2)))
else:
break # other 4xx: not retryable
raise RuntimeError(r.json()["error"]["message"])The official OpenAI and Anthropic SDKs already do all of this — including honoring retry-after — with no configuration. Hand-rolled HTTP clients are where retries usually go missing.
Streaming failures
If a stream cannot complete, it ends with a protocol-shaped error event rather than silently returning an empty response — your client always learns the turn failed. Treat a mid-stream error like a 5xx: retry the whole request with backoff. See Streaming.