401 vs 403

Both look like "you can't have this". The difference is whether the server knows who you are.

401 Unauthorized403 Forbidden
MeansNot authenticated. The request had no credentials, or they were wrong or expired.Authenticated, but not allowed. Who you are is known; this action isn't yours to take.
Retrying with credentialsCan workWon't help
Required headerWWW-AuthenticateNone
Typical causeMissing or expired token, wrong API key, expired sessionWrong role, another tenant's record, read-only key, IP or region rule
Good response body"Authentication required""You don't have access to this project"

The names are historical: 401 should have been called "Unauthenticated", and 403 "Unauthorized". Read them that way and the rest follows.

When a 401 is the right answer

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token"

Send it when no token arrived, the token is malformed, its signature fails, or it has expired. The WWW-Authenticate header is required by the specification and tells the client how to try again. Browsers show their own login box for Basic, which is why APIs normally use Bearer.

When a 403 is the right answer

The credentials are good and the answer is still no: a viewer trying to delete, a key scoped to read-only, one customer's data asked for by another. Say which permission is missing, without leaking whether the record exists.

For "this doesn't exist, and I won't tell you whether it does", 404 is the honest choice, and many APIs return 404 instead of 403 for records in another account.

Debugging both

  1. Look at the request in DevTools → Network → Headers. Is the Authorization header actually there? A redirect, a service worker or a fetch without credentials can drop it.
  2. Decode the token: is it expired, for the wrong audience, or missing a scope? Our JWT decoder runs in your browser.
  3. Check the response's WWW-Authenticate header: real APIs put the reason in error=.
  4. Compare against a request you know works, for example from curl, one header at a time.
  5. A 401 on a cross-origin call is sometimes just a failed preflight: the browser sends OPTIONS without your token, and an authentication check rejects it. Let OPTIONS through.
Send the header and see whether it arrived

HeaderForge sets Authorization on the sites you choose, keeps the value in a variable so it stays out of exported rules, and shows whether the rule actually fired on this tab. Free, no analytics, no account.

Get HeaderForge for Chrome

Related codes worth knowing

More on this topic: Hard refresh in Chrome, and how to really bypass the cache · What is a HAR file, how to open one, and what it exposes · JWT errors: jwt malformed, invalid signature, jwt expired and more · UUID generator