HTTP Status Codes for API Design: A Practical Guide
Every REST API conversation eventually comes down to three digits. A client sends a request, your API sends back a number, and that number does more communication work than almost anything else in the response. Get it right, and clients handle success, retries, and failure without guessing. Get it wrong, and you end up with the industry's most common anti-pattern: a 200 OK wrapped around an error message.
This guide covers the status codes that actually matter for API design, where teams tend to misuse them, and how to apply them consistently.
What Are HTTP Status Codes?
An HTTP status code is a three-digit number a server returns in response to a client's request to say what happened: whether the request succeeded, whether the client needs to change something, or whether the problem was on the server's side. Codes are grouped into five classes by their first digit (1xx to 5xx), and each class carries a distinct signal that a client can act on programmatically, without parsing a response body to work out what went wrong.
RFC 9110 is the core reference for HTTP semantics, including most status codes. The IANA HTTP Status Code Registry is the canonical, continuously updated index, and for codes RFC 9110 doesn't define itself, such as 429, it points to the RFC that does (RFC 6585, in that case). These are the sources worth citing when precision matters, because RFC 9110 supersedes the older RFC 2616 that some references still point to.
The Five Classes of HTTP Status Codes
Before memorizing individual codes, get the classes right. A client that checks only the first digit, rather than the exact code, can still handle a response correctly in most cases. That's the whole point of the class system: it gives clients a coarse-grained fallback when they receive a status code they don't specifically handle.
| Class | Range | Meaning | Common Example |
|---|---|---|---|
| 1xx | 100–199 | Informational: the request was received and processing continues | 100 Continue |
| 2xx | 200–299 | Success: the request was received, understood, and accepted | 200 OK |
| 3xx | 300–399 | Redirection: further action is needed to complete the request | 301 Moved Permanently |
| 4xx | 400–499 | Client error: the request has invalid syntax or can't be fulfilled | 404 Not Found |
| 5xx | 500–599 | Server error: the server failed to fulfill a valid request | 500 Internal Server Error |
1xx Informational
These are interim responses telling the client that processing hasn't finished, without ending the exchange. REST API design rarely touches them directly. They're handled by the HTTP stack (client libraries and servers) below the level where your application code runs, so most API teams never build a 1xx response by hand.
2xx Success
The request was received and processed as intended. This is where the differences between codes matter more than teams expect: 200, 201, and 204 all mean success, but they tell the client different things about what now exists and what to do next.
3xx Redirection
The client needs to take further action, usually by following a different URI, to complete the request. In APIs, these codes appear mostly in versioning and canonical-URL strategies, and less often in everyday resource responses.
4xx Client Error
The request itself is the problem: bad syntax, missing authentication, a resource that doesn't exist, or a request that conflicts with the current state of the resource. This is the class where API design gets opinionated, and where most code review disagreements happen.
5xx Server Error
The request was valid, but something failed on the server while fulfilling it. A well-designed API keeps this class narrow.
If malformed client input reaches an unhandled exception, the resulting 500 accurately describes a real server failure; it isn't a mislabeled client error. So the fix isn't to swap in a 4xx after the fact. The fix is validation: reject the malformed input before it reaches the code path that crashes, so the request never gets the chance to cause that exception.
HTTP Status Codes Every API Developer Should Know
You don't need all 60-plus registered codes in active use. Most production REST APIs rely on a working set of 15 to 20. Here's the set worth knowing, with the specific behavior each one signals.
200 OK
The default success response for GET, PUT, or PATCH. RFC 9110 defines it simply as meaning the request succeeded. Use it for requests that don't create a new resource; that case is what 201 is for.
201 Created
Returned when a request has successfully created a resource, usually after a POST. The response should include a Location header pointing to the new resource, so the client doesn't have to guess the URI it just created.
204 No Content
The request succeeded, but there's nothing to send back. This is the right response for a successful DELETE, or for a PUT where the client doesn't need the updated representation returned.
301 Moved Permanently
The resource has permanently moved to a new URI. For historical reasons, user agents are allowed to change the request method from POST to GET when following a 301, which can silently turn a write into a read for some clients. If the redirected route needs to receive the same method and body, 308 Permanent Redirect is the safer choice for API versioning. Use 301 only when the destination is contract-compatible and you've confirmed how your actual client libraries behave on redirect.
304 Not Modified
Used with conditional requests (an If-None-Match or If-Modified-Since header) to tell the client that its cached copy is still valid. It's worth implementing on high-traffic GET endpoints, because it saves bandwidth on both sides.
400 Bad Request
The server can't or won't process the request because of something the client did: malformed JSON, a missing required field, or a parameter of the wrong type. It's the generic client-error catch-all, and it's often overused for cases that really deserve 401, 403, or 422.
401 Unauthorized
The request lacks valid authentication credentials. Despite the name, this is an authentication failure, not an authorization failure: the client hasn't proven who it is. RFC 9110 makes this more than a naming convention. A server that generates a 401 response MUST send a WWW-Authenticate header with at least one applicable challenge, so the client learns how to authenticate, not just that it needs to.
403 Forbidden
The server understands the request but refuses to fulfill it. This is the authorization failure, but it doesn't require the client to have authenticated first. When credentials were sent, it means the server considers them insufficient for this request, and a server can just as easily return 403 to an anonymous request as to a logged-in one. RFC 9110 allows a client to retry with new or different credentials, but not simply to repeat the request with the same ones. A 403 can also result from reasons that have nothing to do with credentials.
404 Not Found
The requested resource doesn't exist, or at least isn't one the server is willing to reveal. For security-sensitive resources, a common pattern is to return 404 instead of 403, so an unauthorized client can't tell "doesn't exist" apart from "exists but you can't see it."
405 Method Not Allowed
The resource exists but doesn't support the HTTP method used, as when a POST is sent to a read-only endpoint. The origin server MUST (not just should) send an Allow header listing the methods the resource currently supports. A 405 without one leaves the client no way to correct its request.
409 Conflict
The request conflicts with the current state of the target resource. This is the code for attempts to create a duplicate resource and for application-level state conflicts, such as an operation that's only valid when the resource is in a different status from its current one.
Keep it separate from 412 Precondition Failed, which is specifically for a client-supplied conditional header (If-Match, If-Unmodified-Since) that evaluated to false. If a client is racing another client using ETags, a failed condition is a 412. A business-rule conflict with no conditional header involved is a 409.
422 Unprocessable Content
The request is syntactically well-formed but semantically invalid, such as a field with the right type but an out-of-range value. RFC 9110 renamed this code from "Unprocessable Entity" to "Unprocessable Content," which is worth knowing when you're cross-referencing older documentation.
429 Too Many Requests
The client has sent too many requests in a given time window. This code isn't defined in RFC 9110 at all; it comes from the separate RFC 6585. A well-formed 429 response usually includes a Retry-After header telling the client how long to back off.
500 Internal Server Error
A generic signal that something went wrong on the server and no more specific 5xx code applies. Treat it as a last resort, and log enough detail on the server that every 500 is something you can investigate.
502 Bad Gateway
A server acting as a gateway or proxy received an invalid or malformed response from an upstream server it depended on to fulfill the request. Keep it separate from 504 Gateway Timeout: 502 means a response came back but was unusable, while 504 means no response came back in time. If your API sits in front of other services, use 502 when a downstream dependency responds with garbage, and 504 when it doesn't respond at all.
503 Service Unavailable
The server is temporarily unable to handle the request, often because of maintenance or overload. When the downtime is planned, include a Retry-After header so well-behaved clients know when to try again.
Common Mistakes with HTTP Status Codes in API Design
- Returning 200 for everything. The most common failure is an API that always returns
200and hides the real outcome in astatusorsuccessfield in the JSON body. That defeats the purpose of status codes. Clients, proxies, caches, and monitoring tools all read the status code without parsing the body, and an API that hides failures inside a200breaks every one of them. - Confusing 401 and 403. These get swapped all the time.
401means the request doesn't carry valid credentials, so authenticating (or re-authenticating) is the right next step.403means the server understood the request and refuses it; if credentials were sent, it considers them insufficient, so sending the same credentials again won't help, although different ones might. Returning401when you mean403sends a client into a pointless re-login loop. - Returning 500 for client errors. When invalid client input crashes a handler, many frameworks turn the unhandled exception into a
500by default. The500itself is accurate, because an unhandled exception is a genuine server-side failure, and downgrading the status to a4xxwithout fixing anything would just hide a real bug from monitoring. The real fix sits upstream of the status code: add the validation that should have rejected the malformed input with a proper4xxbefore it reached the code that crashed. - Using one generic error code for every 4xx case. An API that returns
400for a missing field, a business-rule violation, and a rate limit alike forces every client to parse error bodies to work out what happened. The more specific codes exist so clients can branch on the status code alone.
Best Practices for Using HTTP Status Codes in Your API
Consistency matters more than perfection. Choose a convention for each case, document it, and apply it the same way across the whole API, rather than optimizing each endpoint on its own.
- Use the narrowest accurate code. A
422tells a client more than a400does. Wherever a more precise code exists, prefer it over the generic catch-all. - Keep error response bodies structured and consistent. The status code carries the category, but a well-designed error body (a machine-readable error code, a human-readable message, and, where relevant, the field that failed) helps clients build useful error handling without guessing at your conventions.
- Rate limit deliberately, not incidentally. If your API enforces limits, return
429with aRetry-Afterheader, rather than a generic400or, worse, a200with a "rejected" flag hidden in the body. - Don't let redirects surprise API clients. Whether a
3xxis followed automatically, and whether the method and body survive the hop, depends entirely on the client library and its configuration. Some follow redirects by default, others need it switched on, and none of this is guaranteed just because a code is technically permanent or method-preserving. Use redirects for genuine URI changes, not as a substitute for a documented version bump, and check the behavior against the client libraries you actually expect to serve. - Log status codes as a first-class signal. A spike in
5xxresponses and a spike in4xxresponses tell you completely different things: the first is your problem, and the second is usually a client integration issue. Accurate codes are what make that signal trustworthy.
Here's a structured error body paired with 422 Unprocessable Content:
{
"error": {
"code": "invalid_field_value",
"message": "The 'quantity' field must be a positive integer.",
"field": "quantity"
}
}
And here's a 201 Created response with its Location header:
HTTP/1.1 201 Created
Location: /orders/48213
Content-Type: application/json
{
"id": "48213",
"status": "pending",
"createdAt": "2026-09-03T10:15:00Z"
}
How WSO2 Standardizes Status Code Handling
Status codes have been part of HTTP since HTTP/1.0 in 1996, and getting them right is less about innovation than consistency. That consistency gets harder once an organization runs APIs across more than one gateway, because different gateway runtimes don't always default to the same error-response conventions.
WSO2 API Platform tackles part of this at the control-plane level rather than the level of individual gateways. The platform's gateway federation brings AWS, Azure, Kong, and Envoy gateways under the same control plane as WSO2's own gateways. It discovers and catalogs their APIs and monitors them for governance compliance, which centralizes how API definitions are managed across a mixed gateway estate.
That isn't the same as uniform status-code behavior automatically appearing on every underlying gateway. Each gateway type still runs its own runtime, and consistent error mapping across all of them depends on how policy is configured and applied through the control plane, not on the control plane simply existing. The distinction matters most for organizations with a genuine multi-gateway setup, whether from mergers, regional deployments, or using different gateways for different workloads, and much less for a team running a single gateway with one convention already in place.
On WSO2's own gateways, the status codes for gateway-generated responses are documented and configurable rather than left to chance:
- The Advanced Rate Limit policy returns
429withRetry-Afterby default, and accepts any code from400to599if your convention is different. - The JWT Authentication policy returns
401by default when a token check fails. - When the gateway can't reach a backend, the request fails with
503after the connect timeout.
That turns status-code behavior at the gateway into a documented contract rather than an implementation detail.
None of this replaces good design discipline on the team building the API. Consistent status codes still start with the choices covered above. What a unified control plane changes is how much of that consistency survives once an API is deployed across more than one type of gateway.
Conclusion
Status codes carry more of an API's meaning than most teams realize. The five classes are the foundation: get them right, and a client can make sensible decisions even about codes it has never seen. Add the 15 to 20 codes that actually come up in REST API design, apply them consistently, and most of the ambiguity behind fragile client integrations disappears. If you're running APIs across more than one gateway, WSO2 API Platform's unified control plane is worth a look for keeping that consistency intact as the number of gateways grows.
Frequently Asked Questions
Is HTTP 429 a rate limit?
Yes. 429 Too Many Requests is the standard status code for rate limiting, defined in RFC 6585. A well-formed 429 response usually includes a Retry-After header telling the client how long to wait before trying again.
How do I rate limit an API to 10 requests per minute?
Rate limiting is a policy decision, usually enforced at the gateway or middleware layer with a sliding window or token bucket algorithm, and it's a separate concern from the status code. Once the limit is reached, the API should return 429 with a Retry-After header, rather than silently dropping or queuing the request.
What do HTTP status codes beginning with 2xx indicate?
Success: the server received, understood, and accepted the request. Which 2xx code to use depends on what happened: 200 for a general success, 201 when a new resource was created, and 204 when there's no content to return.
What is the 204 HTTP status code?
204 No Content means the request succeeded but there's nothing to send back in the response body. It's commonly used for successful DELETE requests, and for PUT or PATCH requests where the client doesn't need the updated resource returned.