Every non-2xx response from the Messages API returns the same JSON envelope: a top-level type: 'error', a nested error object with its own type (the precise category) and a human-readable message, plus a request_id you should log every time. For example: {type: 'error', error: {type: 'invalid_request_error', message: "messages: roles must alternate between 'user' and 'assistant'"}, request_id: 'req_011CSHoEeqs5C35K2UUqR7Fy'}. The HTTP status code narrows the category, error.type names it precisely, and the message almost always identifies the exact field or condition at fault. Debugging starts by reading all three together - not by pattern-matching the status code in isolation.
The Full Error Code Reference
Eight status codes cover essentially every failure mode you'll see:
- 400
invalid_request_error- malformed request: bad JSON, a missing required parameter, an invalid tool schema, messages that don't alternateuser/assistant, or a parameter value out of range. Not retryable as-is; fix the request. - 401
authentication_error- missing, invalid, or revoked API key, or bothANTHROPIC_API_KEYandANTHROPIC_AUTH_TOKENset at once (the SDK sends both headers and the API rejects it). Not retryable without fixing credentials. - 403
permission_error- the key authenticated fine but lacks access to this specific model, beta feature, or organization resource. Not retryable without a different key or a granted permission. - 404
not_found_error- bad endpoint or an invalid/deprecated model ID (a common cause: a dotted ID likeclaude-sonnet-4.6instead of the realclaude-sonnet-4-6). Not retryable unchanged. - 413
request_too_large- request body exceeds the size limit, usually from oversized images or an unbounded conversation history. Not retryable without shrinking the payload. - 429
rate_limit_error- you've exceeded your own account's requests-per-minute, tokens-per-minute, or tokens-per-day limit. Retryable after backing off. - 500
api_error- a transient problem on Anthropic's side. Retryable. - 529
overloaded_error- Anthropic's infrastructure is at capacity, unrelated to your account's usage. Retryable.
request_id is your debugging anchor
Every response, success or failure, carries a request_id (in Python, response._request_id - public despite the underscore). Log it on every failure. It's the first thing Anthropic support asks for, and it's what lets you correlate a user-reported failure with a specific server-side event when the error message alone isn't enough to diagnose.
Retryable vs Non-Retryable - and What the SDK Already Does for You
The official SDKs auto-retry connection errors, 408, 409, 429, and any 5xx with exponential backoff and jitter, by default up to two retries (configurable via max_retries). When a 429 response includes a retry-after header, the SDK honors it instead of guessing a delay. This means a custom retry loop wrapped around the SDK is usually redundant work - and if you do build one, you must not also let the SDK retry underneath it, or a single failure produces a multiplicative retry storm.
The 4xx codes other than 429 - 400, 401, 403, 404, 413 - are deliberately excluded from that automatic retry. They're deterministic: resending an identical malformed request produces an identical failure every time. The only correct response to one of these is to change the request (fix the schema, fix the key, fix the model ID) before sending again, not to resend the same bytes and hope.
Typed Exceptions, Not String Matching
Every SDK maps each status code to its own exception class - in Python, BadRequestError (400), AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), RateLimitError (429), and InternalServerError (5xx), all subclasses of the base APIStatusError, with APIConnectionError covering network failures before any response arrives. Catch from most specific to least specific: except anthropic.NotFoundError: ... except anthropic.RateLimitError: ... except anthropic.APIStatusError as e: ... except anthropic.APIConnectionError: .... Every APIStatusError subclass also exposes a .type property carrying the exact error.type string, useful when two different types share one status code (for example, both permission_error and billing_error can surface as 403).
Common exam distractor
An answer that catches one broad exception class - a single except APIStatusError or catch (AnthropicApiException) - and treats every failure identically is a trap. It discards exactly the distinction that matters: which failures are safe to retry (429, 5xx, connection errors) and which need the request itself fixed (400, 401, 403, 404, 413). Reject any answer that doesn't branch by status or exception type.
401 vs 403 - Authentication vs Authorization
These two get confused constantly. A 401 means the request never proved who it is - the key is missing, malformed, or revoked. A 403 means the request proved exactly who it is, and that identity is real, but it isn't allowed to do the specific thing it asked for - call a model it hasn't been granted, use a beta feature it lacks access to, or touch a resource outside its organization. Seeing a 403 on a model you called successfully five minutes ago (rather than a 401) tells you the key itself is fine; something about access to that specific resource changed.
429 vs 529 - Two Different Sources of 'Slow Down'
Both are retryable, and both mean 'try again later' - but the cause, and therefore the right response, differs. A 429 rate_limit_error means your account has exceeded its own requests-per-minute, tokens-per-minute, or tokens-per-day allowance; check the x-ratelimit-remaining-* and retry-after response headers, and the durable fix is pacing your own request rate or requesting a higher limit. A 529 overloaded_error (or a 500) means Anthropic's infrastructure is capacity-constrained or has a transient fault, entirely independent of your usage - there's no account-side fix, only backoff, and possibly routing to a different model if the workload allows it.
Debugging a 400 on a Tool-Calling Request
A 400 on a request that includes tools is very often a schema problem, not anything to do with the model's behavior: a required field missing from input_schema, an input_schema whose root isn't type: 'object', an unsupported JSON Schema keyword, or (if strict: true is set) a schema missing additionalProperties: false. Because the model never runs before a 400 is raised, nothing about the model's output is implicated at all - the fastest fix comes from reading the exact field the message names, not from re-prompting or guessing based on the status code alone.