Status codes tell the client what happened to its request in one number. This sheet groups them by class and flags the ones that get misused in real APIs — returning 200 on failure is the single biggest offender.
Quick reference
1xx — Informational
| Code |
Name |
Meaning |
| 100 |
Continue |
Client should send the rest of the request body |
| 101 |
Switching Protocols |
Server agrees to switch protocol (used for WebSocket upgrade) |
2xx — Success
| Code |
Name |
Meaning |
| 200 |
OK |
Standard success |
| 201 |
Created |
Resource created (include a `Location` header pointing to it) |
| 202 |
Accepted |
Request accepted for async processing, not yet complete |
| 204 |
No Content |
Success, no response body (common on `DELETE`) |
| 206 |
Partial Content |
Range request fulfilled (video/audio streaming, resumable downloads) |
3xx — Redirection
| Code |
Name |
Meaning |
| 301 |
Moved Permanently |
Resource has a new URL permanently; caches/search engines update their link |
| 302 |
Found |
Temporary redirect (historically often misused for what should be 303/307) |
| 303 |
See Other |
Redirect after a POST — client should GET the new URL |
| 304 |
Not Modified |
Cached version is still valid (`If-None-Match`/`If-Modified-Since`) |
| 307 |
Temporary Redirect |
Like 302 but guarantees the method/body are preserved |
| 308 |
Permanent Redirect |
Like 301 but guarantees the method/body are preserved |
4xx — Client error
| Code |
Name |
Meaning |
| 400 |
Bad Request |
Malformed syntax — the server can't even parse the request |
| 401 |
Unauthorized |
Not authenticated (despite the name, this is about *auth*, not permission) |
| 403 |
Forbidden |
Authenticated, but not allowed to access this resource |
| 404 |
Not Found |
Resource doesn't exist (or the server won't say why) |
| 405 |
Method Not Allowed |
Right URL, wrong HTTP method — include an `Allow` header |
| 409 |
Conflict |
Request conflicts with current state (e.g. version mismatch, duplicate) |
| 410 |
Gone |
Resource existed but was intentionally, permanently removed |
| 422 |
Unprocessable Entity |
Syntactically valid but semantically wrong (failed validation) |
| 429 |
Too Many Requests |
Rate limited — should include a `Retry-After` header |
5xx — Server error
| Code |
Name |
Meaning |
| 500 |
Internal Server Error |
Generic catch-all — something broke server-side |
| 502 |
Bad Gateway |
Upstream/proxy got an invalid response from the origin server |
| 503 |
Service Unavailable |
Server temporarily can't handle the request (overload, maintenance) |
| 504 |
Gateway Timeout |
Upstream/proxy didn't get a response from the origin in time |
Common patterns
Standard API error envelope
{
"error": {
"code": "rate_limited",
"message": "Too many requests, retry after 30s",
"request_id": "req_8f2a1c"
}
}
Pair this body with the matching status code — 429 here, not 200 with an "error" field buried in it.
Correct redirect after a form POST
POST /orders -> 303 See Other
Location: /orders/42
Using 303 (not 302) after a POST guarantees the browser issues a GET for the redirect target instead of potentially re-submitting the POST body.
Conditional GET with caching
GET /report.pdf
If-None-Match: "33a64df5"
304 Not Modified <- no body sent, client uses its cached copy
Pitfalls
- Returning
200 OK with an error in the body: this breaks every generic HTTP client, monitoring tool, and cache that inspects the status line — they'll treat a failed request as a success. The status code is the contract; never make callers parse the body to learn if something failed.
401 vs 403 mixed up constantly: 401 Unauthorized means "I don't know who you are" (missing/invalid credentials); 403 Forbidden means "I know who you are, and you're not allowed." Returning 403 for an expired token trains clients to retry uselessly instead of re-authenticating.
404 used for authorization failures: some APIs return 404 instead of 403 to avoid leaking that a resource exists. That's a deliberate security tradeoff, not a mistake — but document it, because it looks like a bug otherwise.
302 changing the method on redirect: older HTTP clients (and some still today) turn a 302-redirected POST into a GET at the new location, silently dropping the request body. Use 307/308 when you need the method and body preserved.
- No
Retry-After on 429/503: without it, well-behaved clients don't know how long to back off and will hammer the endpoint immediately, defeating the point of the rate limit.
Related ZipKit tools
- JWT Decoder — inspect the claims behind a
401/403 to see exactly why an API rejected a token.
- CSP Header Builder — build the response headers that usually ship alongside a hardened API's status codes.
Related cheatsheets