401 vs 403
Short answer
Return 401 when the request has not proved who it is — no credentials, or invalid or expired ones — because retrying with good credentials could succeed. Return 403 when identity is established and this user still may not do this, because retrying changes nothing. The practical test is whether logging in again could fix it.
Side by side
| 401 | 403 | |
|---|---|---|
| Actually means | Unauthenticated | Unauthorised |
| Server knows who you are | No | Yes |
| Would valid credentials help | Yes | No |
| Requires WWW-Authenticate header | Yes, per the spec | No |
| Typical cause | Missing, invalid or expired token | Valid user without the required permission |
| Sensible client response | Prompt for login, or refresh the token | Show a 'no access' message |
| Cacheable by default | No | Yes |
When to use each
Use 401 when…
No Authorization header, a malformed or expired token, a bad signature, or a session that has timed out. Anything where authenticating properly would let the request through.
Use 403 when…
A logged-in user requesting an admin-only endpoint, a tenant reaching for another tenant's record, or an action blocked by policy — IP restrictions, a suspended account, or file permissions on the server.
The names are the problem
401 is called Unauthorized, but it means unauthenticated — the request has not established who is making it. 403 is called Forbidden and is the one that actually concerns authorisation.
This naming was acknowledged as misleading long ago and kept for compatibility. The HTTP specification itself notes that 401 is about authentication despite the name.
The reliable way to remember it is to ignore the words and ask a question: could logging in with the right account make this request succeed? If yes, it is a 401. If the user is already correctly identified and simply is not permitted, it is a 403.
The WWW-Authenticate requirement
A 401 response is required by the specification to include a WWW-Authenticate header naming the authentication scheme the client should use — Bearer, Basic, or something else.
Most APIs omit it, and most clients do not care, so this is widely ignored in practice. It matters for standards-compliant clients and for browser-native authentication flows, where the header is what triggers the credentials prompt.
If you return 401 from an API, including WWW-Authenticate: Bearer costs nothing and makes the response correct.
When 404 is the better answer
There is a case where neither code is right. Returning 403 for a resource that exists but is not yours confirms that it exists — which leaks information. An attacker enumerating IDs learns exactly which records are real by watching for 403 rather than 404.
For anything sensitive, returning 404 for both 'does not exist' and 'exists but is not yours' reveals nothing. GitHub does this with private repositories: you get a 404, not a 403.
The trade-off is debuggability, since a legitimate user with a genuine permission problem sees a confusing 404. Use 404 where enumeration is a real risk, and 403 where clarity matters more.
Frequently asked questions
Which should I return for an expired token?
What about an API key that has run out of quota?
Should a 403 explain why access was denied?
Why am I getting a 403 on a static file?
Try the HTTP Status Code Lookup
Look up any HTTP status code by number or search by name to see what it means, whether it is cacheable and what usually causes it.