UrsaKit cover for a guide on choosing HTTP status codes for REST APIs

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

CodeUse it forWatch out
200 OKA successful read, update or action that returns a bodyDo not use it for a failure
201 CreatedA POST that created a resourceInclude Location with the new URL
202 AcceptedWork queued but not finished (jobs, exports, webhooks)Return a status URL the client can poll
204 No ContentSuccess with nothing to return (DELETE, PUT with no body)The body must be empty
206 Partial ContentRange requests for mediaRequires 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

CodeMeaningMethod after redirect
301 Moved PermanentlyPermanent moveBrowsers may change POST to GET
302 FoundTemporary moveBrowsers may change POST to GET
307 Temporary RedirectTemporary moveMethod and body preserved
308 Permanent RedirectPermanent moveMethod and body preserved
304 Not ModifiedConditional request matchedReturn 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.

CodeChoose it whenTypical fix the client makes
400 Bad RequestThe request is malformed: broken JSON, wrong types, missing required fieldFix the payload
401 UnauthorizedNo credentials, or the token is expired or invalidAuthenticate again; must send WWW-Authenticate
403 ForbiddenAuthenticated, but not allowedRequest access; do not retry unchanged
404 Not FoundThe resource does not exist, or must not be revealedStop retrying
405 Method Not AllowedThe path exists but not for this methodUse another method; must send Allow
406 Not AcceptableNo representation matches the Accept headerRequest a supported type
409 ConflictCurrent state conflicts: duplicate key, version mismatchRe-read and retry with new state
410 GoneThe resource existed and is permanently goneDrop it from bookmarks and indexes
412 Precondition FailedIf-Match or If-Unmodified-Since failedRe-fetch and retry
413 Content Too LargeThe upload exceeds the limitSend a smaller payload
415 Unsupported Media TypeThe Content-Type is not acceptedSend JSON, not form data
422 Unprocessable ContentSyntax is valid, semantics are notCorrect the values
429 Too Many RequestsRate limit exceededWait 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

CodeMeaningRetryable
500 Internal Server ErrorUnhandled exceptionUsually no; the same request fails again
501 Not ImplementedThe method is not supported by the server at allNo
502 Bad GatewayUpstream returned something unusableSometimes
503 Service UnavailableOverloaded or in maintenanceYes, honour Retry-After
504 Gateway TimeoutUpstream did not answer in timeSometimes, 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

MistakeWhy it hurtsFix
200 for every responseMonitoring, caches and retries all see successUse the code that matches the outcome
401 for permission errorsClients clear the session and loop back to loginUse 403
400 for every validation failureClients cannot distinguish "fix syntax" from "fix values"400 for syntax, 422 for semantics
500 for client errorsPages on-call for a bug in the callerMap validation failures to 4xx
404 for a wrong methodThe client cannot discover the allowed method405 plus Allow
429 without Retry-AfterClients guess and stampedeSend the header
204 with a bodyProxies and clients disagree about the framingReturn 200, or drop the body

07Deciding in three questions

When a handler needs a code, ask in this order:

  1. 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.
  2. 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.
  3. 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.

Frequently 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.

Back to articles

Related articles

Related tools