HTTP 401 vs 403 — Unauthorized vs Forbidden Status Code Comparison

Confused about http 401 vs 403? Learn the exact difference between Unauthorized and Forbidden, when each is returned, and how Express, Stripe, and GitHub APIs use them.

4xx

401

Unauthorized

View full 401 page →

4xx

403

Forbidden

View full 403 page →

Quick Cheat Sheet

Question 401 Unauthorized 403 Forbidden
Who is the caller? Unknown — no/invalid credentials Known — credentials are valid
What's missing? Authentication Authorization (permission)
Should client retry with new creds? Yes No — re-auth won't help
Required response header WWW-Authenticate None

The Core Distinction

Despite the name, 401 Unauthorized is really about authentication, not authorization. RFC 9110 § 15.5.2 says 401 means the request "lacks valid authentication credentials." In contrast, 403 Forbidden (RFC 9110 § 15.5.4) means the server understood who you are but refuses to fulfill the request anyway.

A useful mnemonic: 401 = "I don't know you", 403 = "I know you, and no."

When Each Is Returned

Return 401 when:

  • The Authorization header is missing entirely
  • The bearer token is expired, malformed, or not signed by your issuer
  • A session cookie is invalid or has been revoked

Return 403 when:

  • The token is valid but the user lacks the required role/scope
  • The user owns a resource but the action is disabled by policy
  • The request originates from a blocked IP, country, or rate-limit tier
  • A WAF rule rejects the payload (Cloudflare often returns 403)

Real-World API Examples

  • GitHub API returns 401 Bad credentials for an invalid PAT, but 403 Forbidden when you hit the abuse-detection or secondary rate limits.
  • Stripe API uses 401 Invalid API Key for malformed/revoked keys, and 403 is rare — Stripe prefers 402 Payment Required or 400 for permission-style failures.
  • AWS S3 returns 403 Forbidden for both expired pre-signed URLs and bucket-policy denials, which is a frequent source of confusion.

Common Implementation Mistake

Many APIs return 403 when a token is missing simply because they want to hide the existence of an authentication system. This violates RFC 9110 — if no credentials are presented, the correct status is 401 with a WWW-Authenticate header. If you want to obscure resource existence, return 404 instead.

Real-World Use Case

In an Express.js middleware chain, the auth middleware should return 401 when JWT verification throws (no/invalid token), then a separate authorization middleware should return 403 when the verified user's role doesn't match the required scope. CDNs and WAFs (Cloudflare, AWS WAF) typically use 403 for IP/geo blocks because the caller is identified but denied.

Look Up Any Status Code

Browse all status codes →