Status Codes
HTTP status codes tell the client what happened. They are part of the API contract, not decoration.
Good status codes help clients decide whether to retry, ask the user to log in, show validation errors, report a missing resource, or treat a failure as a server problem.
Basic status groups
<?php
declare(strict_types=1);
$status = 404;
echo match ($status) {
200 => 'OK',
201 => 'Created',
400 => 'Bad Request',
404 => 'Not Found',
default => 'Unexpected status',
} . PHP_EOL;
// Prints:
// Not Found
Status code families:
2xx: success3xx: redirect or not modified4xx: client/request problem5xx: server/provider problem
Common success codes
200 OK is the default for successful reads and many updates.
201 Created means a resource was created. It often includes the created resource or a Location header.
204 No Content means the action succeeded and there is no response body.
<?php
declare(strict_types=1);
function successStatusFor(string $action): int
{
return match ($action) {
'create' => 201,
'delete' => 204,
default => 200,
};
}
echo successStatusFor('create') . PHP_EOL;
// Prints:
// 201
Client error codes
400 Bad Request is useful when the request is malformed, such as invalid JSON.
401 Unauthorized means authentication is missing or invalid. The name is confusing, but it is about authentication.
403 Forbidden means the user is authenticated but not allowed.
404 Not Found means the route or resource was not found. Some APIs also use it to avoid revealing private resources.
405 Method Not Allowed means the path exists, but not for that method.
409 Conflict means the request conflicts with current state, such as a version mismatch or duplicate unique value.
422 Unprocessable Content is commonly used for validation errors where the JSON was syntactically valid but fields failed validation.
429 Too Many Requests means rate limiting.
<?php
declare(strict_types=1);
function statusForProblem(string $problem): int
{
return match ($problem) {
'invalid_json' => 400,
'not_logged_in' => 401,
'not_allowed' => 403,
'missing_resource' => 404,
'validation_failed' => 422,
'rate_limited' => 429,
default => 500,
};
}
echo statusForProblem('validation_failed') . PHP_EOL;
// Prints:
// 422
Server errors
500 Internal Server Error means something unexpected failed.
502 Bad Gateway, 503 Service Unavailable, and 504 Gateway Timeout often appear when proxies, upstream services, or infrastructure are involved.
Do not use 500 for validation errors. If the client sent bad data, use a 4xx code.
Be consistent
There is room for judgement in status codes, especially around 400 vs 422 or 404 vs 403. The key is consistency within the API.
Document the convention and follow it. Clients should not need to guess endpoint by endpoint.
What to check in a project
Check status codes match response bodies.
Check validation errors do not return 200.
Check authentication and authorization use distinct statuses.
Check rate limits use 429 and include useful headers where the project supports them.
Check no-content responses use 204 with an empty body.
Check clients handle non-2xx responses deliberately.
What you should be able to do
After this lesson, you should be able to choose common API status codes, distinguish 400 from 422, 401 from 403, and 404 from 405, and explain how status codes guide client behaviour.
Informational Responses
The 1xx family reports interim protocol progress. Application code rarely sends these directly, but developers may encounter 100 Continue during request-body negotiation or 103 Early Hints in performance-oriented infrastructure. A client must still wait for the final response.
Redirect Semantics
Redirect codes do not all preserve the original method:
301 Moved Permanentlyand302 Foundare widely handled as GET redirects by browsers in historical form workflows.303 See Otherexplicitly tells the client to retrieve another URL with GET and is a strong Post/Redirect/Get choice.307 Temporary Redirectpreserves the method and body.308 Permanent Redirectpreserves the method and body while declaring a permanent move.304 Not Modifiedis not a navigation redirect. It answers a conditional request and carries no representation body.
Additional Client Errors
406 Not Acceptable means the server cannot produce a representation matching the client's Accept header. 408 Request Timeout means the server timed out waiting for the request. 410 Gone says a resource was deliberately removed and is not expected to return.
412 Precondition Failed applies when a conditional request such as If-Match does not match. 415 Unsupported Media Type rejects a request body format the endpoint does not accept. 428 Precondition Required can require conditional updates to prevent lost writes.
For 405 Method Not Allowed, include an Allow header naming supported methods. For 401 Unauthorized, authentication schemes normally use WWW-Authenticate. For 201 Created, use Location when the created resource has a URL. 429 and temporary 503 responses may include Retry-After.
Status, Headers, And Body Form One Contract
Do not choose the status in isolation:
204and304do not carry response bodies.- a response to
HEADsends the headers a corresponding GET would send but omits body bytes; 202means accepted, not completed, so provide a way to observe progress;- a Problem Details or project error body supplements the status instead of replacing it;
- clients should understand unknown codes by their status family.
Retry Decisions
A status code alone does not make a write safe to retry. 408, 429, 502, 503, and 504 can describe temporary conditions, but a repeated create or payment request still needs an idempotency policy. Validation, authorization, and most conflict failures should not be retried unchanged.
Protocol Verification
Test the actual HTTP exchange:
- status line;
- required headers;
- content type;
- body presence or absence;
- behavior through a reverse proxy or CDN;
- client handling of an undocumented code in the correct family.
Status Codes Are Part Of The API Contract
An HTTP status code is not decoration around a response body. It is the first machine-readable statement about what happened. Clients, proxies, caches, SDKs, browsers, monitoring tools, and retry middleware all read the status before they understand any application-specific JSON. A PHP API that returns 200 OK for every outcome forces every client to reverse-engineer failure from custom fields.
Use status codes to describe protocol outcome, then use the body to provide application detail. A validation failure can include field errors. A conflict can include the current version. A rate-limit response can include retry guidance. The status code and the body should agree; a response with 200 and { "success": false } is a mixed contract.
Success Codes
200 OK is the ordinary success response when the server returns a representation. 201 Created says a new resource now exists; include a Location header when the resource has a canonical URL. 202 Accepted says the request was accepted but the work is not complete; include a status resource or polling guidance so the client can discover the final result. 204 No Content says the request succeeded and there is no response body. Do not send JSON with 204.
A successful DELETE can return 204 when no body is useful, or 200 when the API returns a final representation or deletion summary. A successful idempotent update can return 200 with the updated resource or 204 when the client already has what it needs. Pick one convention and document it consistently.
Client Error Codes
4xx responses mean the client request cannot be completed as sent, at least not without changing credentials, input, timing, or resource state. 400 Bad Request fits malformed syntax, unsupported parameters, or a request the server cannot parse according to the API contract. 401 Unauthorized means authentication is missing or invalid; despite the name, it is about authentication. 403 Forbidden means the server understood the identity but refuses the action.
404 Not Found can mean the resource does not exist or the API intentionally hides its existence from this caller. Use that hiding policy consistently. 405 Method Not Allowed tells the client that the resource exists but the method is unsupported, and should include Allow where practical. 409 Conflict fits state conflicts such as version mismatches, duplicate transitions, or trying to cancel an already shipped order. 412 Precondition Failed is useful with conditional requests such as If-Match. 415 Unsupported Media Type and 406 Not Acceptable describe content negotiation failures. 422 Unprocessable Content is often used for semantically invalid JSON after syntactic parsing succeeds; if your API uses it, define its boundary against 400.
Server Error And Retry Codes
5xx responses mean the server or an upstream dependency failed to complete a request that might otherwise be valid. 500 Internal Server Error is a defect or unexpected condition. 502 Bad Gateway and 504 Gateway Timeout often come from proxies or upstream services. 503 Service Unavailable communicates temporary unavailability and can pair with Retry-After when retry timing is known.
Not every 5xx is safe to retry blindly. A timeout after a payment request may be ambiguous: the provider might have processed the charge even though the PHP process did not receive the response. For state-changing operations, combine retry advice with idempotency keys, operation status resources, or reconciliation endpoints.
429 Too Many Requests is a client-side throttling signal rather than a server defect. Include rate-limit headers or a Retry-After value if the API contract supports them. Clients need to know whether waiting can help.
Bodies, Headers, And Methods Must Agree
A status code is only one piece of the exchange. Headers and body shape complete the contract. 201 should identify the created resource. 202 should identify how to track the pending operation. 204 should not carry a body. 401 should use the authentication challenge expected by the API. Cacheable responses need deliberate cache headers.
Method semantics matter too. GET should not create orders. PUT and DELETE are idempotent by HTTP definition, but the application must still implement duplicate handling responsibly. POST can be non-idempotent, so retries need explicit design.
Testing Status Behavior
Build a method/outcome matrix for each important endpoint. Include success, malformed input, validation failure, unauthenticated request, unauthorized identity, missing resource, conflict, rate limit, dependency timeout, and unexpected defect. Contract tests should assert status, headers, and body together.
Use real HTTP tests where possible. A unit test that returns an enum from a handler does not prove middleware, exception mapping, content negotiation, and headers work together. Save representative responses in OpenAPI examples so documentation and behavior stay aligned.
After this lesson, you should be able to choose common HTTP status codes by outcome, keep status, headers, method semantics, and JSON bodies consistent, distinguish retryable from ambiguous failures, and verify status-code behavior through real API exchanges.
Practice
Task: Choose API Status Codes
Write a small PHP script that maps common API outcomes to status codes.
Requirements
- Use
declare(strict_types=1);. - Include outcomes for created, deleted, invalid JSON, validation failure, unauthenticated, forbidden, missing resource, and rate limited.
- Return the status code and a short label.
- Print at least five outcomes.
Check Your Work
Run the script and confirm authentication and authorization are not treated as the same status.
Show solution
This solution keeps the mapping explicit so status-code choices are easy to review.
<?php
declare(strict_types=1);
/**
* @return array{status: int, label: string}
*/
function statusForOutcome(string $outcome): array
{
return match ($outcome) {
'created' => ['status' => 201, 'label' => 'Created'],
'deleted' => ['status' => 204, 'label' => 'No Content'],
'invalid_json' => ['status' => 400, 'label' => 'Bad Request'],
'validation_failed' => ['status' => 422, 'label' => 'Unprocessable Content'],
'unauthenticated' => ['status' => 401, 'label' => 'Unauthorized'],
'forbidden' => ['status' => 403, 'label' => 'Forbidden'],
'missing_resource' => ['status' => 404, 'label' => 'Not Found'],
'rate_limited' => ['status' => 429, 'label' => 'Too Many Requests'],
default => ['status' => 500, 'label' => 'Internal Server Error'],
};
}
foreach (['created', 'deleted', 'validation_failed', 'unauthenticated', 'forbidden', 'rate_limited'] as $outcome) {
$result = statusForOutcome($outcome);
echo $outcome . ': ' . $result['status'] . ' ' . $result['label'] . PHP_EOL;
}
// Prints:
// created: 201 Created
// deleted: 204 No Content
// validation_failed: 422 Unprocessable Content
// unauthenticated: 401 Unauthorized
// forbidden: 403 Forbidden
// rate_limited: 429 Too Many Requests
Why This Works
The examples show successful creation, successful deletion, validation failure, authentication failure, authorization failure, and rate limiting as separate outcomes.
Practice: Choose Redirect And Conditional Statuses
Choose statuses and required headers for Post/Redirect/Get, a method-preserving temporary move, an ETag cache hit, and a stale optimistic update.
Your answer must identify the intended behavior, the important failure case, and the evidence that proves the result.
Show solution
Use 303 with Location after a successful form POST, 307 for a temporary method-preserving redirect, 304 with validators and no body for an ETag match, and 412 or the documented API conflict convention for a failed If-Match update.
Verify the real response, deployment, or workload rather than relying only on configuration text.
Practice: Review A Status, Header, And Body Contract
Review responses that return JSON with 204, omit Allow from 405, return 401 without an authentication challenge, and send 201 without identifying a created resource.
Your answer must identify the intended behavior, the important failure case, and the evidence that proves the result.
Show solution
Remove the body from 204; add Allow to 405; add the appropriate WWW-Authenticate challenge to 401; and include Location for 201 when the created resource has a stable URL. Test emitted bytes and headers.
Verify the real response, deployment, or workload rather than relying only on configuration text.