HTTP Status Code Picker
Not sure which HTTP status code to return? Answer a few questions and this interactive decision tree will find the right one for your API response.
When a 200 Still Means Something Went Wrong
A recurring anti-pattern in API design is an endpoint that returns 200 OK for every request it successfully processed, regardless of whether the underlying operation actually succeeded — the real outcome gets encoded instead in a JSON field inside the body, something like {"status": "declined"}. GraphQL-over-HTTP is a well-known real-world example of this by design: nearly every GraphQL response returns 200 even when the query itself failed, which is exactly why GraphQL gateways and monitoring tooling had to be built to parse response bodies rather than trust transport status. A client that always reads the full body handles this fine. Anything downstream that inspects only the HTTP status code — a load balancer's health check, a CDN's cache-control logic, an uptime monitor, a webhook consumer routing on status — cannot see the failure at all, because as far as the transport layer is concerned, the request succeeded.
The problem generalizes beyond any single field: HTTP status codes exist precisely so that generic infrastructure between a server and its clients can make correct decisions without understanding the application's business logic. The moment a business-level failure — a declined charge, a rejected validation, a failed downstream dependency — is wrapped in a 200 response, every HTTP-aware layer in that path loses the ability to react to it: it won't retry, won't alert, and won't distinguish "worked" from "didn't." Reserving 2xx exclusively for genuine success and routing everything else through the matching 4xx or 5xx class keeps that signal intact for every intermediary that depends on it, which is the whole reason status codes exist as a channel separate from the response body in the first place.
The RFCs That Define These Numbers
HTTP status codes are standardized in the IETF's HTTP semantics specification — originally RFC 2616, later split and refined into RFC 7231, and consolidated again into RFC 9110 — which defines the five numeric classes (1xx through 5xx) and the specific meaning of each registered code. Some codes used constantly in modern APIs were added well after the original HTTP/1.1 spec: 429 Too Many Requests arrived in RFC 6585 in 2012 to standardize rate-limit signaling, and 422 Unprocessable Content originated in WebDAV's RFC 4918 before being adopted broadly by REST APIs for semantic validation failures. The IANA maintains the canonical registry of all assigned status codes, and well-behaved HTTP clients, proxies, and caches are expected to fall back to the general behavior of a code's class (2xx succeeded, 4xx client's fault, 5xx server's fault) even for codes they don't specifically recognize — which is exactly why picking a code from the correct class matters more than memorizing every registered number.
Slip-Ups That Keep Showing Up in Code Review
- Returning 200 with an error payload: as the story above illustrates, this defeats every HTTP-aware intermediary — load balancers, CDNs, and monitoring tools all treat 2xx as success and won't alert or retry, no matter what the response body says.
- Using 403 instead of 404 to hide resource existence: returning
403 Forbiddenfor a resource id that belongs to another tenant confirms to an attacker that the id is valid; returning404 Not Foundfor both "doesn't exist" and "exists but you can't see it" is the safer default in multi-tenant systems. - Sending 429 without a
Retry-Afterheader: without it, clients are left guessing a backoff interval instead of respecting the one the server actually wants, often making the retry storm worse rather than better. - Returning a fresh 201 on a retried, already-processed POST: if a client retries after a timeout and the resource was already created, returning another 201 (and creating a duplicate) instead of 200 or 409 with the existing resource breaks idempotency guarantees clients depend on.
Picking a Code: A Concrete Walkthrough
Say you're building a DELETE /orders/{id} endpoint and need to decide what to return in three scenarios. If the order exists and is deleted successfully, the tree's "success" branch asks whether there's anything to return — since a DELETE typically has no body, the answer lands on 204 No Content, not 200 OK. If the order id doesn't exist at all, the tree's "client error" branch asks what's wrong with the request, and "resource doesn't exist" resolves to 404 Not Found. Now suppose the order exists but is already in a "shipped" state that your business rules forbid deleting — this isn't a missing resource and it isn't malformed input, so neither 404 nor 400 fits; walking the "client error" branch to "validation failed, correct syntax, wrong values" lands on 422 Unprocessable Content, with a response body explaining that shipped orders can't be deleted. Three requests to the same endpoint shape, three different correct codes, decided entirely by what actually happened server-side rather than by habit.
Moments That Call for the Status Code Tree
- Designing a brand-new endpoint: deciding between 200, 201, 202, and 204 for operations with different side effects before the API contract is finalized and hard to change.
- Reviewing a pull request: quickly checking that error branches use the right code — especially the 400-vs-422 and 401-vs-403 distinctions that are easy to get backwards under review time pressure.
- Debugging an integration a client reports as "broken": confirming the server is returning the status code that matches what actually happened, rather than a catch-all 200 or 500 that obscures the real failure mode.
- Writing or auditing API documentation: checking that the status codes listed in the docs match the semantics actually implemented, rather than copy-pasted defaults from a template.
Frequently Asked Questions
What is the difference between HTTP 401 and 403?
401 Unauthorized signals that the request lacks valid authentication credentials — the server doesn't know who the caller is. The response should include a WWW-Authenticate header telling the client how to authenticate. 403 Forbidden signals that the server knows who the caller is but refuses to grant access — the resource exists but the caller doesn't have permission to see it. A common security pattern is to return 404 Not Found instead of 403 when you want to conceal the existence of a resource from unauthorized callers.
When should I use 422 instead of 400?
Use 400 Bad Request when the request itself is malformed at the protocol or format level — the JSON body is unparseable, a required header is absent, or the content type is wrong. Use 422 Unprocessable Content when the request is syntactically correct but fails business logic validation — the JSON parses fine but a field value violates a constraint, such as an end date earlier than a start date, a price below zero, or a username that's already taken. This distinction helps API clients distinguish between "fix your serialisation code" (400) and "fix your input data" (422).
What is the difference between 301 and 302 redirects?
301 Moved Permanently tells the client (and search engines) that the resource has permanently moved to the new URL — the client should update any bookmarks and search engines should transfer ranking signals to the new URL. 302 Found (also called "Moved Temporarily") tells the client the resource is temporarily at a different URL but will return to the original — the client should keep using the original URL for future requests. Use 308 Permanent Redirect and 307 Temporary Redirect when you also need to preserve the original HTTP method (POST stays POST) through the redirect.