
In brief
The status code is part of your API's contract: it tells clients whether to retry, re-authenticate, fix the request or show an error. Guessing it breaks retries and caches.
Most API bugs that survive review are not logic bugs — they are status code bugs. A handler returns 200 with a { "error": ... } body, and suddenly the mobile client shows a success screen, the CDN caches the failure for ten minutes, and the retry library never fires. The status line is the part of the response that infrastructure reads; the body is what your application reads.
This guide walks through the codes you actually choose between, in the order you decide them. You can look up any code in the UrsaKit HTTP status code reference, which links each one to its RFC.
01Status codes are a contract, not a label
A status code has three readers, and only one of them is your application:
- Caches decide whether to store the response based on 2xx vs 4xx vs 5xx, and on headers such as
Cache-Control. - Proxies and gateways treat 5xx as an origin failure, which may trigger a circuit breaker.
- Client libraries decide whether to retry, refresh a token, or surface an error to the user.
HTTP/1.1 201 Created
Location: /v1/orders/8f14e45
Content-Type: application/json
{ "id": "8f14e45", "status": "pending" }
Nobody parses that status line for fun; they parse it because the retry logic depends on it.
022xx: what the client may assume succeeded
| Code | Use it for | Watch out |
|---|---|---|
200 OK | A successful read, update or action that returns a body | Do not use it for a failure |
201 Created | A POST that created a resource | Include Location with the new URL |
202 Accepted | Work queued but not finished (jobs, exports, webhooks) | Return a status URL the client can poll |
204 No Content | Success with nothing to return (DELETE, PUT with no body) | The body must be empty |
206 Partial Content | Range requests for media | Requires Content-Range |
The distinction worth getting right is 200 versus 202. If the client must poll, the request is not finished, and 202 says so before the client starts wondering whether the write was lost.
033xx: redirects and caching
| Code | Meaning | Method after redirect |
|---|---|---|
301 Moved Permanently | Permanent move | Browsers may change POST to GET |
302 Found | Temporary move | Browsers may change POST to GET |
307 Temporary Redirect | Temporary move | Method and body preserved |
308 Permanent Redirect | Permanent move | Method and body preserved |
304 Not Modified | Conditional request matched | Return no body, reuse the cached copy |
For API endpoints, prefer 307 or 308: they are the two codes that guarantee the method and body survive the hop. 301 and 302 have a long history of being rewritten to GET by clients.
044xx: the client's request is the problem
This is where most of the design decisions live, because "bad request" is a family rather than a code.
| Code | Choose it when | Typical fix the client makes |
|---|---|---|
400 Bad Request | The request is malformed: broken JSON, wrong types, missing required field | Fix the payload |
401 Unauthorized | No credentials, or the token is expired or invalid | Authenticate again; must send WWW-Authenticate |
403 Forbidden | Authenticated, but not allowed | Request access; do not retry unchanged |
404 Not Found | The resource does not exist, or must not be revealed | Stop retrying |
405 Method Not Allowed | The path exists but not for this method | Use another method; must send Allow |
406 Not Acceptable | No representation matches the Accept header | Request a supported type |
409 Conflict | Current state conflicts: duplicate key, version mismatch | Re-read and retry with new state |
410 Gone | The resource existed and is permanently gone | Drop it from bookmarks and indexes |
412 Precondition Failed | If-Match or If-Unmodified-Since failed | Re-fetch and retry |
413 Content Too Large | The upload exceeds the limit | Send a smaller payload |
415 Unsupported Media Type | The Content-Type is not accepted | Send JSON, not form data |
422 Unprocessable Content | Syntax is valid, semantics are not | Correct the values |
429 Too Many Requests | Rate limit exceeded | Wait for Retry-After |
Three of these carry a mandatory header, and omitting it turns a helpful response into a confusing one: 401 needs WWW-Authenticate, 405 needs Allow, and 429 should send Retry-After.
401 and 403 are not interchangeable.
401 means "I do not know who you are" and the client should authenticate. 403 means "I know who you are and you still cannot do this" — re-authenticating will not help, so clients that treat the two the same end up in a login loop.
The 403 versus 404 question is a security decision. If resource existence is itself sensitive — another tenant's order ID, an unpublished document — return 404 so the response cannot be used to enumerate what exists. If the client is allowed to know the resource exists but not to touch it, 403 is more honest.
055xx: the server or its dependencies failed
| Code | Meaning | Retryable |
|---|---|---|
500 Internal Server Error | Unhandled exception | Usually no; the same request fails again |
501 Not Implemented | The method is not supported by the server at all | No |
502 Bad Gateway | Upstream returned something unusable | Sometimes |
503 Service Unavailable | Overloaded or in maintenance | Yes, honour Retry-After |
504 Gateway Timeout | Upstream did not answer in time | Sometimes, with a timeout budget |
Return 503 with a Retry-After during maintenance windows and overload. Without that header, well-behaved clients fall back to exponential backoff, and agents that implement HTTP 402 for machine payments show why explicit machine-readable signals beat guessing.
Never wrap a real failure in 200.
A 200 with an error object breaks caching, hides the failure from monitoring and disables the retry logic that already exists in every HTTP client. If the response carries an error, the status code must say so.
06Common mistakes worth fixing
| Mistake | Why it hurts | Fix |
|---|---|---|
| 200 for every response | Monitoring, caches and retries all see success | Use the code that matches the outcome |
| 401 for permission errors | Clients clear the session and loop back to login | Use 403 |
| 400 for every validation failure | Clients cannot distinguish "fix syntax" from "fix values" | 400 for syntax, 422 for semantics |
| 500 for client errors | Pages on-call for a bug in the caller | Map validation failures to 4xx |
| 404 for a wrong method | The client cannot discover the allowed method | 405 plus Allow |
429 without Retry-After | Clients guess and stampede | Send the header |
| 204 with a body | Proxies and clients disagree about the framing | Return 200, or drop the body |
07Deciding in three questions
When a handler needs a code, ask in this order:
- Did the server do what was asked? If yes, the code is 2xx — 200, 201, 202 or 204 depending on whether something was created, queued or returned.
- If not, is it the caller's fault? A malformed request is 400; missing or bad credentials are 401; authenticated but not allowed is 403; a missing resource is 404; a state conflict is 409; valid syntax with invalid values is 422; too many requests is 429.
- If not, is it our fault? A bug is 500, an unusable upstream is 502, an overloaded or maintained service is 503 with
Retry-After.
Then check the contract details: Location on 201, Allow on 405, WWW-Authenticate on 401, Retry-After on 429 and 503, and an empty body on 204.
Use the HTTP status code reference to confirm a code before you ship it — the description is written for the person deciding, not for the spec lawyer. And if the error body is JSON, run it through the JSON validator too: an error response that fails to parse is worse than no error response at all.
Every 1xx–5xx code with a plain-language explanation and the RFC it comes from.
HTTP Status CodesFrequently asked questions
Should an API return 404 or 403 for a resource the user cannot access?
Return 404 when revealing existence would itself leak information, which is the usual choice for per-tenant data. Return 403 when the client already knows the resource exists and simply lacks permission.
Is 200 with an error body acceptable?
No. A 200 tells every intermediary, cache and retry library that the call succeeded. Clients then have to parse the body to discover the failure, and monitoring never sees it.
When should I use 422 instead of 400?
Use 400 when the request is malformed — bad JSON, missing required field, wrong type. Use 422 when the syntax is valid but the semantics are not, such as an end date before a start date.
What is the difference between 401 and 407?
401 means the request lacks valid credentials for the target resource and must include a WWW-Authenticate header. 407 is the proxy equivalent and uses Proxy-Authenticate.
Which codes should a client retry?
408, 425, 429 and 503 are explicitly retryable, ideally honouring Retry-After. 500 and 502 usually indicate a server-side bug, so retrying blindly repeats the failure.
Can a 204 response have a body?
No. 204 means no content, so the body must be empty. If you need to return the deleted object, use 200 with a body instead.
Related articles
- How to format, validate and minify JSON
Keep the error body as machine-readable as the success body.
- Compare two texts with a diff checker
Compare two failing API responses to find the field that changed.
Related tools
- HTTP Status Codes
Look up any code and what clients do with it.
- JSON Formatter & Validator
Validate the payload that ships with the status code.
- Base64 Encoder & Decoder
Inspect authorization headers and tokens.
- Regex Tester
Match and rewrite log lines by status code.