401 and 403 are not interchangeable, and confusing them causes real bugs in clients.
401 Unauthorized actually means *unauthenticated*. The name is a historical mistake in the HTTP specification. It means: I do not know who you are. A client should obtain or refresh a credential and may retry.
403 Forbidden means: I know who you are, and the answer is no. Retrying with the same credential will never succeed.
A client that receives 401 for a scope failure will refresh its token and retry, get 401 again, refresh again, and loop until it hits a retry limit — burning quota and producing a confusing failure.
The failure modes
| Scenario | Status | Usual cause |
|---|---|---|
| No credential | 401 | Header omitted entirely |
| Malformed header | 401 | Missing scheme, or an unexpanded shell variable |
| Unrecognised credential | 401 | Test key sent to production — the most common cause |
| Expired token | 401 | Client did not refresh proactively |
| Insufficient scope | 403 | Token valid, permission absent |
| IP not allowed | 403 | Egress address not on the allowlist |
| Origin not allowed | 403 | Browser-restricted key from an unregistered origin |
| Rate limited | 429 | Budget exhausted; send Retry-After |
Send WWW-Authenticate on 401
RFC 7235 requires it, and it is genuinely useful: the error and error_description parameters tell a client whether to refresh or to give up.
WWW-Authenticate: Bearer realm="api", error="invalid_token",
error_description="The access token expired"Without it a client cannot distinguish "your token expired, refresh it" from "that key does not exist, stop trying".
Do not confirm which keys exist
Return the same response for an unknown key and a wrong key. If an unknown key gives 401 and a revoked one gives 403, an attacker can enumerate which keys were ever valid.
The same reasoning applies to error text. "Invalid credentials" for everything credential-related. Reserve specific messages for cases where the caller is already authenticated.
Test the failure paths
Every generated response comes with client test code, because authentication error handling is almost never tested and almost always subtly wrong. The two tests that matter most: a 401 triggers exactly one refresh, and a 403 triggers none.
Frequently asked questions
Should a missing API key be 401 or 403?
What about a valid key with no permission for the resource?
Should the response body say why authentication failed?
For token *format* problems — expired, malformed — yes, because the client can act on it. For "does this key exist", no. Be specific about things the legitimate caller can fix, vague about things only an attacker benefits from knowing.
Is 404 ever right instead of 403?
Sometimes deliberately. Returning 404 for a resource the caller is not permitted to see hides its existence, which matters for private repositories or documents. It is a real trade-off: it obscures information at the cost of a confusing experience for a legitimate user who has lost access.