When a request to the Claude API is refused for access reasons, the response tells you precisely which kind of refusal it is. Every error body has the same shape — a top-level "type": "error", an error object with type and message, and a request_id you can quote to support — and the three access-failure variants map cleanly to who has to act.
The taxonomy
| HTTP code | Error type | What it means | Who fixes it |
|---|---|---|---|
| 401 | authentication_error | The API key is missing, malformed, revoked, or simply wrong — the request could not be attributed to a valid identity | Credential owner: rotate or correct the key, check the header it's sent in |
| 402 | billing_error | The identity is valid, but the account has a billing problem blocking usage | Billing admin: resolve payment or plan status in the Console |
| 403 | permission_error | The identity is valid and billing is fine, but this key lacks permission for this resource or operation | Org/workspace admin: adjust key scope, workspace, or role |
The distinction matters operationally because all three look identical to a naive retry loop: the request "failed." But per Anthropic's retry guidance, 4xx access errors are not retryable — the official SDKs auto-retry 429s, 5xx errors, and connection failures with exponential backoff, and deliberately do not retry these. A 401 retried a thousand times is still a 401; worse, an alerting system that lumps them in with transient errors hides a hard outage ("every request is failing auth") inside a metric that usually means "the API is busy."
Catching them distinctly in code
The Python SDK raises typed exceptions, so you can branch without inspecting status codes: AuthenticationError for 401 and PermissionDeniedError for 403, with BadRequestError, NotFoundError, RateLimitError, and friends alongside. Billing errors don't get their own class, but every APIStatusError subclass exposes a .type property carrying the API's error-type string — check it for "billing_error":
from anthropic import Anthropic, AuthenticationError, \
PermissionDeniedError, APIStatusError
client = Anthropic()
try:
msg = client.messages.create(model="claude-sonnet-5",
max_tokens=256, messages=[{"role": "user", "content": "Hi"}])
except AuthenticationError:
alert("401: rotate/verify the API key") # credentials desk
except PermissionDeniedError:
alert("403: key lacks scope for this route") # workspace admin
except APIStatusError as e:
if e.type == "billing_error":
alert("402: billing blocked — finance") # billing desk
else:
raise
Catch the most specific exception first; the base classes exist as fallbacks, not first resorts.
What 403 means changes by platform
On third-party platforms the same status code points at platform-specific configuration, so the "who fixes it" column shifts:
Claude Platform on AWS: a 403 means the request reached the server and was rejected — the usual suspects are a wrong workspace_id or a missing IAM action on the calling principal. Note also that first-party sk-ant-api... keys do not work there at all; keys are issued in the AWS Console. See the Platform-on-AWS setup errors guide.
Microsoft Foundry: with Entra ID authentication, 403s usually mean a missing Azure RBAC role such as Cognitive Services User on the Foundry resource. Foundry also returns two correlation IDs — request-id (Anthropic) and apim-request-id (Azure API Management) — and Anthropic advises quoting both to support. Foundry error codes has the full picture.
type string, not just the HTTP code, and route alerts by it. authentication_error pages the on-call for secrets management; billing_error notifies finance and engineering leadership; permission_error opens a ticket with whoever owns workspace or IAM configuration. None of them should feed a retry queue.Where to go next
The full error-code reference covers the rest of the taxonomy (429, 500, 529, and the streaming-specific cases), and SDK error handling goes deeper on exception hierarchies across languages.