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.
The On-Call Page That Started With a 200
A payments team once shipped an endpoint that always returned 200 OK, even when a charge failed — the actual outcome was encoded in a JSON field inside the body, {"status": "declined"}. It worked fine for the web frontend, which parsed the body correctly. Then a downstream reconciliation service, written by a different team, was added to watch for failed payment webhooks by checking the HTTP status code alone, on the reasonable assumption that a REST API would use HTTP semantics correctly. Every declined charge was silently counted as successful for weeks, because nothing in the transport layer signaled failure — monitoring dashboards, the load balancer's health metrics, and the reconciliation job all agreed the requests were fine.
The discrepancy was only caught when finance flagged a mismatch between processor settlement reports and the internal ledger. The postmortem's single largest recommendation was blunt: status codes must reflect what actually happened at the HTTP layer, and a JSON body is not a substitute for HTTP semantics that every intermediary in the request path — proxies, gateways, monitoring agents — already knows how to interpret.
Where These Numbers Actually Come From
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.
Where Teams Reach for the 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.