HTTP Status Codes Every API Developer Should Know
A practical tour of HTTP status codes — 2xx success, 3xx redirects, 4xx client errors, 5xx server errors. Which to return when, and the common mistakes to avoid.
What you'll learn
- ✓The five status code categories and what each one means
- ✓The handful of 2xx, 4xx, and 5xx codes you will actually use
- ✓When 3xx redirects fire and when to use them
- ✓The classic mistakes: 200 OK with an error body, 401 vs 403, 400 vs 422
- ✓A cheat sheet you can keep open while you build
Prerequisites
- •A rough idea of HTTP requests and responses
- •Useful background: What Is REST?
Every HTTP response carries a three-digit status code. It’s the first piece of information clients see, and it’s the API’s primary way of saying what happened. Most developers can name a dozen — but the codes are designed to be precise, and reaching for the right one is part of building a professional API.
This post is a working tour of the codes that matter, grouped by category.
The five categories
The first digit tells you the class:
| Range | Class | Meaning |
|---|---|---|
| 1xx | Informational | Rare in practice; mostly protocol-level |
| 2xx | Success | The request succeeded |
| 3xx | Redirection | The client needs to go somewhere else |
| 4xx | Client error | The request was wrong |
| 5xx | Server error | The server failed |
The split between 4xx and 5xx is the most important contract you offer to clients. A 4xx says “don’t retry blindly — fix your request.” A 5xx says “this might work next time.”
2xx — success
The three codes you will use 99% of the time.
200 OK
The generic success. Use for a successful GET, PUT, PATCH, or any other operation that returns a body.
HTTP/1.1 200 OK
Content-Type: application/json
{ "id": 42, "name": "Alice" }
201 Created
A new resource was created. Always include a Location header pointing to it, and typically the new resource in the body:
HTTP/1.1 201 Created
Location: /users/42
Content-Type: application/json
{ "id": 42, "name": "Alice" }
Use 201 for a POST that creates a resource. Use 200 for a POST that performs an action (/orders/42/cancel).
204 No Content
Success, but there’s nothing to return. The classic case is a successful DELETE:
HTTP/1.1 204 No Content
No body, no Content-Type header. Some APIs return 200 with {} instead — that’s fine, but 204 is more idiomatic and saves a few bytes.
Less common 2xx
- 202 Accepted — the request was received but processing happens asynchronously. Return a job URL the client can poll.
- 206 Partial Content — the response is part of a larger resource. Used for range requests (resumable downloads, video streaming).
3xx — redirection
Redirects fire when the resource lives somewhere else. APIs use them less than websites do, but they have their place.
301 Moved Permanently
The resource has a new permanent URL. Browsers and crawlers cache this aggressively.
HTTP/1.1 301 Moved Permanently
Location: /v2/users/42
Use sparingly. Once you ship a 301, search engines and bookmarks remember it forever.
302 Found (and 307 / 308)
Temporary redirect — try this other URL for now, but the original URL is still canonical. 302 has slightly fuzzy semantics around method preservation; 307 and 308 are stricter alternatives:
- 307 Temporary Redirect — like 302, but the client must reuse the same method.
- 308 Permanent Redirect — like 301, but the client must reuse the same method.
If you’re building a JSON API, you probably want 307 or 308 over 302.
304 Not Modified
The client sent a conditional request (If-None-Match or If-Modified-Since) and the resource hasn’t changed. The server returns 304 with no body, telling the client to use its cached copy.
GET /users/42
If-None-Match: "abc123"
HTTP/1.1 304 Not Modified
ETag: "abc123"
304 is the foundation of HTTP caching. If you set ETag or Last-Modified headers on responses, clients can revalidate cheaply.
4xx — client errors
The request was wrong. The client should read the response, fix the request, and either retry or give up.
400 Bad Request
The request was malformed or failed validation. Use this when the client sent something the server can’t process.
POST /users
Content-Type: application/json
{ "name": "Alice" }
HTTP/1.1 400 Bad Request
{ "error": { "code": "missing_field", "message": "email is required" } }
400 is the catch-all for “client made a mistake.” When in doubt, 400 is rarely wrong.
401 Unauthorized
The request lacks valid credentials. This is about authentication, not authorization. The name is famously misleading.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api"
Return 401 when:
- No token / API key was sent
- The token is invalid, expired, or revoked
403 Forbidden
The client is authenticated, but not allowed. This is about authorization.
HTTP/1.1 403 Forbidden
{ "error": { "code": "insufficient_scope", "message": "Token lacks write:users" } }
The rule of thumb:
- 401 — “I don’t know who you are.”
- 403 — “I know who you are; you can’t do that.”
Some APIs return 404 instead of 403 to avoid leaking the existence of a resource. That’s a security trade-off, not a bug.
404 Not Found
The resource doesn’t exist. Use for unknown IDs and unknown routes.
GET /users/9999
HTTP/1.1 404 Not Found
{ "error": { "code": "user_not_found", "message": "No user with id 9999" } }
A 404 should not be returned for unauthorised resources unless you’re hiding their existence deliberately.
405 Method Not Allowed
The resource exists, but the method isn’t supported. Include an Allow header listing what is:
DELETE /users
HTTP/1.1 405 Method Not Allowed
Allow: GET, POST
409 Conflict
The request collides with the current state of the resource. The textbook case is a duplicate:
POST /users
{ "email": "alice@example.com" }
HTTP/1.1 409 Conflict
{ "error": { "code": "email_taken", "message": "That email is already registered" } }
Also used for optimistic concurrency failures (the resource was modified since you last read it).
410 Gone
The resource used to exist but is permanently gone. Stronger than 404 — tells clients to stop asking.
422 Unprocessable Entity
The request was syntactically valid but semantically wrong. This is the source of the eternal “400 vs 422” debate.
- 400 — “I couldn’t parse your request” (bad JSON, missing field).
- 422 — “I parsed it fine, but the values don’t make sense” (negative price, end date before start date).
In practice, many APIs use 400 for everything client-side and never reach for 422. That’s fine — pick one convention and use it consistently. The Rails and Laravel communities tend to use 422 heavily; the Stripe / Twilio / Google styles lean on 400.
429 Too Many Requests
The client hit a rate limit. Include Retry-After:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{ "error": { "code": "rate_limited", "message": "Try again in 60 seconds" } }
A polite client will back off. A polite server will tell it how long.
Try it yourself. For each scenario, write down the right 4xx code:
- The request body is
{— invalid JSON - No
Authorizationheader on a private endpoint - The user is logged in but isn’t an admin and tried to delete another user
PATCH /users/42where user 42 doesn’t existPOST /userswith an email that’s already in use- The client sent 1,001 requests in the last minute and the limit is 1,000
Answers: 400, 401, 403, 404, 409, 429.
5xx — server errors
Something went wrong on the server. The client did nothing wrong (or at least, you can’t tell).
500 Internal Server Error
The catch-all. Use when an exception bubbles up that you don’t have a more specific code for.
HTTP/1.1 500 Internal Server Error
{ "error": { "code": "internal_error", "message": "Something went wrong", "requestId": "req_abc123" } }
Never leak stack traces or SQL errors to clients. Log them server-side with a request ID; return that ID so support can correlate.
502 Bad Gateway
You’re a proxy or gateway and the upstream service returned something invalid. Common in API gateways, load balancers, and serverless platforms when a function crashes.
503 Service Unavailable
The server is up but can’t handle the request right now — overload, maintenance, deployment in progress. Include Retry-After:
HTTP/1.1 503 Service Unavailable
Retry-After: 30
503 is what a load balancer returns when no healthy upstream is available.
504 Gateway Timeout
You’re a gateway and the upstream took too long. Different from 503 — the upstream might be alive but slow.
| Code | ”Who failed?" | "When?“ |
|---|---|---|
| 500 | The server itself | The handler crashed |
| 502 | The upstream | Bad response from upstream |
| 503 | The server | Overloaded or down |
| 504 | The upstream | Upstream timed out |
Common mistakes
These are the ones that come up in code review again and again.
200 with an error body
HTTP/1.1 200 OK
{ "error": "User not found" }
This breaks every HTTP-aware tool: monitoring dashboards, retry libraries, browser DevTools. The whole point of status codes is that they’re machine-readable. Don’t undermine them. Return 404 instead.
500 for invalid input
If the client sent garbage and you didn’t validate, you’ll see a 500. That’s a 400. Validate early; let bad input bounce off the edge of your application.
401 vs 403 confusion
The naming is bad, but the distinction is real:
- 401: prove who you are
- 403: I know who you are, you still can’t
If you can’t decide, ask “would different credentials fix this?” If yes, 403. If no creds were sent, 401.
Using 200 for “no result”
A GET that finds nothing is a 200 with an empty array ([]), not a 404. The collection exists; it’s just empty.
GET /users?role=admin
HTTP/1.1 200 OK
[]
A 404 is for “the resource you addressed doesn’t exist,” not “your filter matched nothing.”
Inventing your own codes
Don’t return 299 or 469. HTTP defines the codes you can use; intermediaries (proxies, browsers, CDNs) only understand the official ones. Stick to standard codes plus a code field in your error envelope for finer detail — see REST API Design for the error envelope pattern.
A printable cheat sheet
2xx Success
200 OK — generic success with a body
201 Created — POST created a resource
204 No Content — success with no body (DELETE)
3xx Redirection
301 Moved Permanently
304 Not Modified — cached copy is still valid
307/308 Temporary/Permanent (method-preserving)
4xx Client error
400 Bad Request — malformed or invalid
401 Unauthorized — missing/bad credentials
403 Forbidden — credentials valid, not allowed
404 Not Found — no such resource
405 Method Not Allowed
409 Conflict — collides with current state
422 Unprocessable Entity (optional alternative to 400)
429 Too Many Requests — rate limited
5xx Server error
500 Internal Server Error
502 Bad Gateway — upstream gave a bad response
503 Service Unavailable — overloaded or down
504 Gateway Timeout — upstream timed out
That’s roughly 15 codes. Master those and you can build any HTTP API.
Try it yourself. Open an API you’ve worked with and audit one endpoint per status class:
- A successful GET — is it 200 with a body, or 204 with none?
- A failed auth — does it return 401 when no token, 403 when token is wrong scope?
- A 500 — does the response include a request ID for support?
Three small audits will catch most of the cleanup opportunities.
Recap
You now know:
- The five classes of status codes and the contract each one offers
- 200, 201, 204 cover almost every success
- 301, 304, 307/308 are the redirects worth knowing
- 400, 401, 403, 404, 409, 422, 429 cover client errors
- 500, 502, 503, 504 distinguish what failed on the server side
- The common pitfalls: 200-with-error, 500-for-bad-input, 401/403 confusion
Next steps
Status codes pair naturally with HTTP methods — knowing both is the foundation of any REST work. Continue with HTTP Methods Explained for the verbs side of the same picture, and bring it all together with REST API Design: Practical Best Practices.
Questions or feedback? Email codeloomdevv@gmail.com.