HTTP Status Codes for API Developers: Choosing the Right One
A status code is the first thing a client reads and often the only thing it acts on automatically. Retry logic, caching, error reporting, and monitoring dashboards all branch on it. Returning 200 OK with {"error": "not found"} in the body breaks every one of those tools at once.
There are more than sixty registered status codes, but an API uses about fifteen of them regularly. This guide focuses on those, and especially on the pairs that are easy to confuse. The definitions come from RFC 9110 (HTTP Semantics), which replaced the older RFC 7231 in 2022.
The five classes
| Range | Meaning | Who should act |
|---|---|---|
| 1xx | Informational | Rarely seen by application code |
| 2xx | Success | Nobody — it worked |
| 3xx | Redirection | The client follows it or uses its cache |
| 4xx | Client error | The caller must change the request |
| 5xx | Server error | The server has a problem; retrying may help |
The 4xx/5xx split is the most important one. It answers "whose fault is this?", and clients use it to decide whether a retry could possibly succeed. Retrying a 400 is pointless, because the same request will fail the same way. Retrying a 503 after a delay is often exactly right.
Success: 200 vs 201 vs 202 vs 204
- 200 OK — the general success response, with a body.
- 201 Created — a new resource was created, typically by a
POST. Include aLocationheader pointing at the new resource, and usually the resource itself in the body. - 202 Accepted — the request was accepted but the work hasn't finished, as with a queued export or a video transcode. Return something the client can poll, such as a job URL.
- 204 No Content — success with deliberately no body. Common for
DELETEand for updates where the client doesn't need the result back. A 204 response must not include a body; some clients fail or hang when one is sent anyway.
Bad input: 400 vs 422
400 Bad Request means the server can't process the request because of something wrong with it — often that the request can't even be parsed, such as malformed JSON or a missing required header.
422 Unprocessable Content means the request was syntactically fine but semantically invalid: well-formed JSON whose email field isn't an email address, or an end date before the start date. It was originally a WebDAV code and is now part of the core HTTP specification.
Both are defensible for validation errors, and many successful APIs use 400 for everything. What matters more is consistency and a useful body. Say which field failed and why, in a machine-readable shape. RFC 9457 ("Problem Details for HTTP APIs") defines a standard JSON format for this, with type, title, status, and detail fields, which you can extend with a list of per-field errors.
Auth: 401 vs 403
These two are confused more than any other pair, partly because 401's official name, "Unauthorized", really means unauthenticated.
- 401 Unauthorized — the server doesn't know who you are. Credentials are missing, expired, or invalid. The fix is to (re)authenticate. A 401 response is required to include a
WWW-Authenticateheader describing how to authenticate. - 403 Forbidden — the server knows exactly who you are, and you aren't allowed to do this. Logging in again won't help.
The practical difference: a client receiving a 401 should try refreshing its token or send the user to log in. A client receiving a 403 should show a "you don't have access" message. Getting this wrong produces login loops, where a user with a valid session but insufficient permissions is sent back to the login screen forever.
Missing things: 404 vs 410, and hiding existence
404 Not Found means nothing exists at this URL, or the server won't say whether it does. 410 Gone means it existed and has been permanently removed, which is useful for retired API versions and deleted public content, since it tells crawlers and clients not to try again.
For private resources, it's often correct to return 404 instead of 403. If GET /invoices/9812 returns 403 for another customer's invoice, you've confirmed that invoice 9812 exists. Returning 404 reveals nothing. RFC 9110 explicitly permits this.
Conflicts and preconditions: 409 and 412
- 409 Conflict — the request conflicts with the resource's current state: creating a user whose username is already taken, or trying to transition an order from "shipped" back to "cart".
- 412 Precondition Failed — used with conditional requests. If a client sends
If-Match: "etag-v3"and the resource has since changed to v4, return 412. This is the standard way to prevent two people's edits from silently overwriting each other.
Rate limiting: 429
429 Too Many Requests tells the client to slow down. Always include a Retry-After header, either in seconds or as an HTTP date, so well-behaved clients know exactly how long to wait instead of guessing or hammering you with immediate retries.
Caching: 304 Not Modified
When a client sends If-None-Match with an ETag it already has, and the resource hasn't changed, respond with 304 and no body. The client reuses its cached copy. On large, frequently polled resources, this can dramatically reduce bandwidth.
Server errors: 500 vs 502 vs 503 vs 504
| Code | Meaning | Typical cause |
|---|---|---|
| 500 Internal Server Error | The server hit an unexpected condition | An unhandled exception in your code |
| 502 Bad Gateway | A proxy got an invalid response from upstream | The app crashed or closed the connection behind a load balancer or reverse proxy |
| 503 Service Unavailable | Temporarily unable to handle requests | Maintenance, overload, or a deliberate circuit breaker; send Retry-After |
| 504 Gateway Timeout | A proxy didn't get an upstream response in time | A slow database query or a hung downstream service |
When you're debugging, the distinction tells you where to look. A 500 carries your application's own error. A 502 or 504 usually comes from the reverse proxy in front of it, such as nginx, Caddy, or a cloud load balancer, so the proxy's logs are where the real reason lives.
Redirects in APIs: 301/302 vs 307/308
Historically, many clients changed a POST into a GET when following a 301 or 302, silently dropping the request body. 307 (temporary) and 308 (permanent) were introduced to forbid that: the method and body must be preserved. For API endpoints that accept writes, prefer 307 and 308.
A compact decision guide
- Did it work? Use 200, 201 for creation, 202 for queued work, or 204 for no body.
- Is the caller unauthenticated? Use 401. Authenticated but not allowed? Use 403, or 404 to hide existence.
- Is the request malformed? Use 400. Well-formed but invalid? Use 422, or 400 consistently.
- Does it conflict with current state? Use 409. Did a precondition fail? Use 412.
- Too many requests? Use 429 with
Retry-After. - Did the server fail? Use 500 for your code, 503 for deliberate unavailability, and let proxies emit 502 and 504.
When you're testing an API, look at the status code and headers before the body. They usually tell you which of the categories above you're in, and therefore where the problem lives. For more on testing APIs, see choosing between REST, GraphQL, gRPC, and WebSockets.