Development

How should APIs use status codes for RESTful responses?

Learn RESTful API best practices for using HTTP status codes to provide clear semantics and predictable behavior.

By Inventive HQ Team

A RESTful API should map each outcome to the most specific status code that describes it: 2xx when the request succeeded (200 for a body, 201 for a newly created resource plus a Location header, 202 for accepted-but-async, 204 for success with no body), 3xx for redirection and cache validation (301/308 permanent, 302/307 temporary, 304 unchanged), 4xx when the client is at fault (400 malformed, 401 unauthenticated, 403 authenticated-but-forbidden, 404 missing, 409 conflict, 422 valid-syntax-but-invalid-data, 429 rate-limited), and 5xx when the server failed (500 unexpected, 502/504 upstream problems, 503 temporarily unavailable). The status line is the machine-readable contract; the response body is a human-readable supplement, never a substitute.

That's the summary an AI Overview will give you. What it can't show you is the judgment calls that actually break real integrations — 401 vs 403, 400 vs 422, 404 vs 403 for hidden resources, and how to signal partial success in a batch. Below is a live status-code lookup you can search, a ranked pick-one-code reference table, a decision-flow diagram, and per-verb (GET/POST/PUT/PATCH/DELETE) code maps you can copy straight into an API style guide.

Loading interactive tool...

The one-code-per-situation reference table

Most "which status code?" arguments come down to a handful of near-collisions. This table gives the single recommended code for each common situation, the runner-up code teams also use, and the header or body detail that makes the choice unambiguous. Codes marked with the RFC that defines them are stable and safe to rely on: 9110 is the core HTTP semantics spec (2022), 6585 adds 429, and 4918 (WebDAV) is where 422 and 207 originate.

SituationUse this codeAlso seenThe detail that decides it
GET returns existing data200 OKBody present; cacheable
POST creates a new resource201 Created200Must include Location: to the new resource
Request accepted, work runs async202 Accepted201Location: points to a job/status URL, not the resource
Success but no body to send (DELETE, PUT)204 No Content200No body at all — clients must not parse one
Client not logged in / bad token401 UnauthorizedSend WWW-Authenticate; re-auth may fix it
Logged in but lacks permission403 Forbidden404Re-auth won't help; return 404 to hide existence
Well-formed request, resource missing404 Not Found410Use 410 Gone if it existed and was deleted
Malformed syntax (bad JSON, bad ID type)400 Bad Request422The parser failed before validation ran
Valid syntax, fails business rules422 Unprocessable Entity400Email formatted correctly but already taken
Write conflicts with current state409 Conflict412Use 412 Precondition Failed with If-Match ETags
Client exceeded rate limit429 Too Many Requests503Always send Retry-After; 503 implies server fault
Unexpected server bug500 Internal Server ErrorLog a request_id; never leak stack traces
Upstream/proxy returned garbage502 Bad Gateway504504 if upstream timed out instead of erroring
Down for maintenance / overloaded503 Service Unavailable500Send Retry-After; it's explicitly temporary
Batch with mixed success/failure207 Multi-Status200Per-item status codes live in the body

Which should I use when the code is ambiguous? Pick the code that lets the client decide its next action without reading the body: 401 says "re-authenticate," 403 says "stop," 429 says "wait Retry-After seconds," 409 says "re-fetch and retry," and 500 says "report a bug." If two codes would trigger the same client behavior, pick the more common one and document it.

The 401 vs 403 vs 404 decision, visualized

The single most common status-code mistake is collapsing authentication, authorization, and existence into one code. Walk a request left-to-right: each gate answers one question, and the first gate that fails picks the code.

Auth decision flow for choosing 401, 403, or 404 A request passes through three gates — authenticated, authorized, exists — and the first failing gate selects 401, 403, or 404; passing all three returns 200. Request arrives Authenticated? valid token? Authorized? has permission? Exists? resource found? 200 OK yes yes yes 401 no / bad token 403 no permission 404 not found

The 404-to-hide trick matters for security: if returning 403 on a resource the user can't see would confirm that the resource exists, return 404 instead so unauthorized clients can't enumerate private records. Document whichever convention you pick — clients need consistency more than they need theoretical purity.

Core RESTful Status Code Rules

2xx Success - Operation Succeeded

200 OK: Successful request with response body

GET /users/{id}
200 OK
{"id": 123, "name": "John", "email": "john@example.com"}

PUT /users/{id}
200 OK
{"id": 123, "name": "John Updated", "email": "john@example.com"}

201 Created: Successful resource creation

POST /users
201 Created
Location: /users/{id}
{"id": 124, "name": "Jane", "email": "jane@example.com"}

Key: Include Location header pointing to new resource

202 Accepted: Request accepted for asynchronous processing

POST /reports/generate
202 Accepted
Location: /reports/jobs/789
{"status": "processing", "job_id": "789"}

204 No Content: Successful request with no response body

DELETE /users/{id}
204 No Content

PUT /settings
204 No Content

Rule: Use appropriate 2xx code to clearly signal success type.

3xx Redirection - Client Action Required

301 Moved Permanently: Resource permanently relocated

GET https://api.example.com/v1/users
301 Moved Permanently
Location: https://api.example.com/v2/users

[Client should update to use new URL]

302 Found: Temporary redirect

GET https://example.com/users
302 Found
Location: https://api.example.com/users

[Client may update to new location]

304 Not Modified: Resource unchanged since last request

GET /data HTTP/1.1
If-None-Match: "abc123"

304 Not Modified

[Client uses cached copy]

307 Temporary Redirect: Like 302, but preserves HTTP method

POST /form
307 Temporary Redirect
Location: /form-processor

[Browser re-POSTs to new location, not GET]

Rule: Use 3xx when client action needed to complete request.

Advertisement

4xx Client Error - Client's Fault

400 Bad Request: Malformed request

POST /users
400 Bad Request

{"error": "Invalid JSON: missing closing brace"}

401 Unauthorized: Missing or invalid authentication

GET https://api.example.com/admin
401 Unauthorized

{"error": "Missing authentication token"}

403 Forbidden: Authenticated but unauthorized

GET https://api.example.com/admin
403 Forbidden

{"error": "User role 'viewer' lacks access to admin"}

404 Not Found: Resource doesn't exist

GET /users/{nonexistent-id}
404 Not Found

{"error": "User not found"}

409 Conflict: Request conflicts with current state

PUT /document/123
409 Conflict

{"error": "Document was modified; refresh and retry"}

422 Unprocessable Entity: Valid format, invalid content

POST /users
422 Unprocessable Entity

{"error": "Email already in use"}

429 Too Many Requests: Rate limit exceeded

GET https://api.example.com/search
429 Too Many Requests
Retry-After: 60

{"error": "Rate limit exceeded"}

Rule: 4xx indicates client made a mistake; client must correct and retry.

5xx Server Error - Server's Fault

500 Internal Server Error: Unexpected server error

GET /data
500 Internal Server Error

{"error": "Internal error occurred"}

502 Bad Gateway: Upstream service returned invalid response

GET https://api.example.com/data
502 Bad Gateway

{"error": "Upstream service error"}

503 Service Unavailable: Server temporarily unavailable

GET https://api.example.com/data
503 Service Unavailable
Retry-After: 300

{"error": "Server maintenance in progress"}

504 Gateway Timeout: Upstream service too slow

GET https://api.example.com/data
504 Gateway Timeout

{"error": "Upstream service timeout"}

Rule: 5xx indicates server error; client might retry later.

RESTful Operation Patterns

GET - Reading Resources

GET /users                     → 200 OK (list of users)
GET /users/{id}                → 200 OK (single user)
GET /users/{nonexistent}       → 404 Not Found
GET /users?updated-since=date  → 304 Not Modified (if unchanged)
GET /users (auth required)     → 401 Unauthorized or 403 Forbidden

Best practice: Always use 200 OK for successful GET.

POST - Creating Resources

POST /users                    → 201 Created + Location header
POST /users (invalid data)     → 400 Bad Request or 422 Unprocessable
POST /users (duplicate)        → 409 Conflict
POST /async-job                → 202 Accepted + Location for tracking
POST /protected               → 401 Unauthorized or 403 Forbidden

Best practice: Return 201 with Location header pointing to created resource.

PUT - Replacing Resources

PUT /users/{id}                → 200 OK (return updated resource)
PUT /users/{id}                → 204 No Content (if not returning body)
PUT /users/{id} (not found)   → 404 Not Found or 201 Created (for create)
PUT /users/{id} (conflict)    → 409 Conflict (if versioned)
PUT /protected                → 401 or 403

Best practice: Return 200 OK with updated resource or 204 No Content.

PATCH - Partial Updates

PATCH /users/{id}              → 200 OK (return updated resource)
PATCH /users/{id}              → 204 No Content
PATCH /users/{id} (conflict)  → 409 Conflict
PATCH /users/{id} (invalid)   → 400 Bad Request or 422 Unprocessable

Best practice: Similar to PUT; return updated resource or 204.

DELETE - Deleting Resources

DELETE /users/{id}             → 204 No Content
DELETE /users/{id}             → 200 OK (with deletion confirmation)
DELETE /users/{nonexistent}    → 404 Not Found
DELETE /users/{id} (conflict)  → 409 Conflict (has dependencies)
DELETE /protected              → 401 or 403

Best practice: Return 204 No Content (safest) or 200 OK with confirmation.

Status Code Decision Tree

START: Client makes request

Does request succeed?
├─ YES
│  └─ Is this a resource creation (POST)?
│     ├─ YES → 201 Created (with Location header)
│     ├─ NO  → Is there response content?
│        ├─ YES → 200 OK
│        ├─ NO  → 204 No Content
│     ├─ Is this async processing?
│        └─ YES → 202 Accepted (with tracking Location)
│
├─ NO
│  ├─ Is the request format bad?
│  │  └─ YES → 400 Bad Request
│  │
│  ├─ Is this missing/invalid authentication?
│  │  └─ YES → 401 Unauthorized
│  │
│  ├─ Is user authenticated but not authorized?
│  │  └─ YES → 403 Forbidden
│  │
│  ├─ Does the resource not exist?
│  │  └─ YES → 404 Not Found
│  │
│  ├─ Does content validation fail?
│  │  └─ YES → 422 Unprocessable Entity
│  │
│  ├─ Does the request conflict with current state?
│  │  └─ YES → 409 Conflict
│  │
│  ├─ Did the server have an unexpected error?
│  │  └─ YES → 500 Internal Server Error
│  │
│  ├─ Is the server temporarily unavailable?
│  │  └─ YES → 503 Service Unavailable

Error Response Format Best Practices

Consistent Error Structure

{
  "error": "human-readable message",
  "code": "MACHINE_READABLE_CODE",
  "status": 400,
  "timestamp": "2025-01-31T10:00:00Z",
  "request_id": "req-abc-123",
  "details": {
    "field": "email",
    "reason": "Email already registered"
  }
}

Validation Error Response

{
  "error": "Validation failed",
  "code": "VALIDATION_ERROR",
  "status": 422,
  "errors": [
    {
      "field": "email",
      "message": "Invalid email format"
    },
    {
      "field": "age",
      "message": "Must be 18 or older"
    }
  ]
}

Consistency Across Endpoints

Bad - Inconsistent error formats:

GET /users → {"error": "Not found"}
POST /users → {"message": "User not found"}
DELETE /users → {"error_message": "User not found"}

Good - Consistent error format:

GET /users → {"error": "User not found", "code": "NOT_FOUND"}
POST /users → {"error": "User not found", "code": "NOT_FOUND"}
DELETE /users → {"error": "User not found", "code": "NOT_FOUND"}

Handling Common Scenarios

Bulk Operations

For bulk operations, multiple status codes may apply:

POST https://api.example.com/users/bulk
[create 10 users, 7 succeed, 3 fail with duplicates]

Option 1 - Partial Success:
202 Accepted
{
  "created": 7,
  "failed": 3,
  "errors": [
    {"index": 2, "error": "Duplicate email"}
  ]
}

Option 2 - Fail Completely:
422 Unprocessable Entity
{
  "error": "Bulk operation partially failed",
  "details": [...]
}

Recommendation: Document behavior clearly. Partial success (202) more user-friendly.

Long-Running Operations

POST https://api.example.com/export/large-dataset
202 Accepted
Location: https://api.example.com/jobs/{jobId}
{
  "status": "processing",
  "job_id": "abc-123",
  "progress": 0
}

[Client polls Job endpoint]
GET https://api.example.com/jobs/{jobId}
200 OK
{
  "status": "processing",
  "progress": 45
}

[When complete]
GET https://api.example.com/jobs/{jobId}
200 OK
{
  "status": "completed",
  "progress": 100,
  "download_url": "https://cdn.example.com/exports/dataset-abc-123.csv"
}

Conditional Requests

GET /data HTTP/1.1
If-None-Match: "abc123"

Server: Is data unchanged?
├─ YES → 304 Not Modified [no body]
└─ NO  → 200 OK [with current data]

Rate Limiting

GET https://api.example.com/search
429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706779200

{"error": "Rate limit exceeded"}

API Versioning with Status Codes

Deprecating API Versions

GET https://api.example.com/v1/users (old)
301 Moved Permanently
Location: https://api.example.com/v2/users

[Tells clients to migrate]

vs.

GET https://api.example.com/v1/users (deprecated)
200 OK
Deprecation: true
Sunset: Sun, 31 Dec 2025 23:59:59 GMT

[Warns about upcoming removal]

Use 301: When old version should never be used Use 200 with warnings: When deprecation window needed

Testing Status Code Implementation

Test Matrix

Operation    | Normal | Auth Fail | Conflict | Not Found
GET          | 200    | 401/403   | N/A      | 404
POST (create)| 201    | 401/403   | 409      | N/A
PUT          | 200    | 401/403   | 409      | 404
PATCH        | 200    | 401/403   | 409      | 404
DELETE       | 204    | 401/403   | 409      | 404

Common Test Cases

def test_api_status_codes():
    # GET existing resource
    assert get("/users/{id}").status == 200

    # GET non-existing resource
    assert get("/users/{nonexistent}").status == 404

    # POST create resource
    response = post("/users", {"name": "John"})
    assert response.status == 201
    assert "Location" in response.headers

    # POST with auth failure
    assert post("/users", {}, headers={}).status == 401

    # DELETE
    assert delete("/users/{id}").status == 204

    # Conditional request
    response1 = get("/data")
    etag = response1.headers["ETag"]
    response2 = get("/data", headers={"If-None-Match": etag})
    assert response2.status == 304

Conclusion

Proper HTTP status code usage in RESTful APIs is fundamental to creating predictable, usable services. By following these practices:

  1. Use appropriate 2xx codes for success (200, 201, 202, 204)
  2. Use 3xx for redirects and conditional responses
  3. Use 4xx for client errors (400, 401, 403, 404, 409, 422, 429)
  4. Use 5xx for server errors
  5. Provide consistent error response formats
  6. Include helpful response headers
  7. Document status code meanings

APIs that use status codes properly are more usable, more testable, and can be automated more effectively. Invest time getting this right from the beginning of API design.

Frequently Asked Questions

What is the difference between 200 OK and 201 Created?

Use 200 OK for successful requests that return existing data (GET) or update existing resources (PUT/PATCH). Use 201 Created specifically when a new resource is created (POST), always including a Location header pointing to the new resource. The distinction helps clients understand whether they're working with existing data or something newly created, enabling proper caching and redirect handling.

When should I use 204 No Content vs 200 OK?

Use 204 No Content when the operation succeeds but there's intentionally no response body to return—common for DELETE operations or PUT updates that don't need to echo back the resource. Use 200 OK when you have a response body to return. Using 204 instead of 200 with an empty body is semantically clearer and saves bandwidth, signaling to clients they shouldn't expect or parse a response.

What is the difference between 400 Bad Request and 422 Unprocessable Entity?

Use 400 Bad Request for syntactically invalid requests—malformed JSON, missing required headers, or unparseable request bodies. Use 422 Unprocessable Entity when the syntax is correct but the data fails business validation—like an email that's properly formatted but already registered, or a date that's in the past when future dates are required. Some APIs use 400 for both, but 422 provides more semantic precision.

Should I use 401 Unauthorized or 403 Forbidden for access denied?

Use 401 Unauthorized when authentication is missing or invalid—the user hasn't logged in, or their token is expired/invalid. Use 403 Forbidden when authentication succeeded but the user lacks permission for this specific resource or action. The key difference: 401 means "who are you?" while 403 means "I know who you are, but you can't do this." Re-authenticating might fix 401 but won't fix 403.

When should an API return 404 Not Found vs 400 Bad Request?

Use 404 Not Found when a well-formed request targets a resource that doesn't exist (GET /users/99999 when user 99999 doesn't exist). Use 400 Bad Request when the request itself is malformed (GET /users/abc when IDs must be numeric). A 404 tells clients the request format is correct but the specific resource is missing; 400 tells them to fix their request format.

What status code should I return for rate limiting?

Return 429 Too Many Requests when a client exceeds rate limits. Always include a Retry-After header indicating when they can retry (in seconds or as an HTTP-date). Include rate limit headers in all responses (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) so clients can throttle proactively. Avoid using 503 Service Unavailable for rate limiting—that implies server-side issues, not client behavior.

How should APIs handle partial success in batch operations?

For batch operations with mixed results, use 207 Multi-Status (WebDAV) with per-item status codes in the response body, or return 200 OK with a detailed response showing individual outcomes. Avoid 200 if everything failed or 400 if some succeeded. For all-or-nothing transactions, use 200 for full success or 4xx/5xx for complete failure. Document your batch semantics clearly—clients need to know if operations are atomic.

What is the difference between 500, 502, 503, and 504 server errors?

500 Internal Server Error is a generic server failure—unexpected exceptions, bugs, or unhandled errors. 502 Bad Gateway means your server (acting as proxy) received an invalid response from upstream. 503 Service Unavailable indicates temporary overload or maintenance—include Retry-After header. 504 Gateway Timeout means an upstream server didn't respond in time. The distinctions help clients decide whether to retry immediately, wait, or report bugs.

Should REST APIs always return the same status codes for the same operations?

Yes, consistency is crucial for API usability. Document your status code conventions and apply them uniformly. If POST returns 201 for one resource type, it should return 201 for all created resources. Create an API style guide defining which codes you use for which scenarios. Inconsistent status codes force clients to handle special cases, increasing integration complexity and bugs.

How do I communicate validation errors with proper status codes?

Return 400 Bad Request or 422 Unprocessable Entity with a structured error response body. Include: an error code (machine-readable), a message (human-readable), and a details array with field-level errors showing the field name, rejected value, and specific validation failure. Example: {"error": "validation_failed", "message": "Invalid input", "details": [{"field": "email", "message": "Invalid email format"}]}. This enables clients to display targeted error messages.

REST APIHTTP status codesAPI designbest practicesweb services