---
title: "API Status Code Decisions Cheatsheet"
slug: "api-status-codes"
category: "cheatsheets"
tags: ["cheatsheets", "http", "api", "rest", "error-handling"]
status: "stable"
last_updated: 2026-09-27
summary: "Action-to-status decision table for REST API endpoints (create, update, delete, validation, auth, rate limits, async jobs), plus the status code mistakes to avoid and a standard error body."
related:
  [
    "[[cheatsheets/http-status-codes]]",
    "[[backend/fastapi]]",
    "[[comparisons/rest-vs-graphql]]",
    "[[cheatsheets/curl-test-patterns]]",
    "[[frontend/nextjs-route-handlers]]",
  ]
---

## Overview

Use this card when writing an endpoint and deciding what to return. It maps the action the handler performed to one status code and the header that should travel with it. For the meaning of every individual code, see [[cheatsheets/http-status-codes]]. Framework-level error handling for Python APIs is in [[backend/fastapi]].

## Pick the status from the action the endpoint performed

The most common API decisions in one place.

| Action                                 | Status                                     |
| -------------------------------------- | ------------------------------------------ |
| Created a resource                     | 201 with `Location:` header.               |
| Updated a resource (no body returned)  | 204.                                       |
| Updated a resource (body returned)     | 200.                                       |
| Deleted a resource                     | 204 (or 200 with confirmation body).       |
| Validation failed on JSON body         | 422 with field errors.                     |
| Missing required header or query param | 400.                                       |
| User not authenticated                 | 401 with `WWW-Authenticate:` header.       |
| Authenticated user lacks permission    | 403.                                       |
| Resource missing                       | 404 (or 410 if it was deleted on purpose). |
| Duplicate insert / version conflict    | 409.                                       |
| Rate limit hit                         | 429 with `Retry-After:`.                   |
| Async job accepted                     | 202 with `Location:` to a status URL.      |
| Maintenance window                     | 503 with `Retry-After:`.                   |

## Avoid the common status code gotchas

- `200 OK` with `{"error": "..."}` in the body is wrong. The status code is part of the API.
- `401 Unauthorized` is really "unauthenticated." Use `403` when the user is known but lacks permission.
- Many clients rewrite `POST` to `GET` on a `302`. Use `303` for the Post-Redirect-Get pattern or `307` for "same method."
- Returning `200` from a `POST` that created a resource loses information that `201` carries.
- `404` and `410` are both "not here." Use `410` when the URL is intentionally retired so caches and crawlers stop trying.
- `Retry-After:` is a header that pairs with `429` and `503`. Clients honor it; omitting it forces blind retries.

## Return a structured error body with every 4xx and 5xx

The status code tells the client what class of failure happened; the body tells it which field or rule failed. RFC 9457 (Problem Details for HTTP APIs) defines a standard shape served as `application/problem+json`:

```json
{
  "type": "https://example.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "email must be a valid address",
  "instance": "/users"
}
```

Add extension members such as an `errors` array for per-field messages. Keep the `status` member identical to the HTTP status line, and never put stack traces in the body.

## Related

- [[cheatsheets/http-status-codes]]
- [[backend/fastapi]]
- [[comparisons/rest-vs-graphql]]
- [[cheatsheets/curl-test-patterns]]
- [[frontend/nextjs-route-handlers]]
