HTTP Status Codes: Complete List with API Testing Examples
The status code is the first thing every client reads in an API response, and the first thing a tester should assert. A login API that answers 200 OK with “invalid password” in the body, or a typo in a search that crashes with 500, are real bugs that ship to production every day. This lesson is your complete HTTP status codes reference for API testing: every code you will meet, what it means in real life, and how to test it on live endpoints.
The 5 Classes of HTTP Status Codes
| Class | Meaning | Real-life analogy (courier delivery) | Who should act |
|---|---|---|---|
| 1xx Informational | Request received, continue | “Parcel picked up, in process” | Nobody, usually invisible |
| 2xx Success | It worked | “Delivered” | Client uses the result |
| 3xx Redirection | Look somewhere else | “Customer moved, forwarded to new address” | Client follows the new location |
| 4xx Client error | Your request is wrong | “Incomplete address” or “no one authorised to receive” | Client must fix the request |
| 5xx Server error | Server failed | “Our van broke down” | Server team must fix; client may retry |
Try Any Status Code Live
The practice API has a helper endpoint that returns whatever status you ask for: GET /status/{code}. Use it to check how your client, Postman tests or Playwright framework handle each code. Here is a 503 with a Retry-After header:
https://sqatools.in/wp-json/sqa-api/v1/status/503curl -i \
"https://sqatools.in/wp-json/sqa-api/v1/status/503"Edit the URL to /status/201, /status/204, /status/409 and more. An invalid code such as /status/700 returns 400.
2xx Success Codes
| Code | Name | When to expect it | What to assert |
|---|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH; POST actions that don’t create (e.g. login, search) | Body contains the expected data |
| 201 | Created | POST (or PUT) created a new resource | Location header + new id in the body |
| 202 | Accepted | Async work queued (report export, bulk upload) | Job id; poll until done |
| 204 | No Content | DELETE, or update with nothing to return | Body is truly empty |
| 206 | Partial Content | Range requests (video streaming, resumable downloads) | Content-Range header |
200 vs 201 vs 204
- Create an order →
201 Created(not 200). The practice API’sPOST /objectsfollows this rule. - Fetch an order →
200 OK. - Delete an order →
204 No Content.
HTTP 200 with body {"success": false, "message": "Invalid OTP"}. Monitoring sees no errors, caches may store the failure, and clients that only check status think it worked. Report it: the status should be a 4xx.3xx Redirection Codes
| Code | Name | Real example | Keeps method and body? |
|---|---|---|---|
| 301 | Moved Permanently | Old API v1 URL permanently moved; SEO-safe redirect | Not guaranteed (clients may switch POST to GET) |
| 302 | Found (temporary) | Login redirect to SSO page | Not guaranteed |
| 303 | See Other | After POST payment, redirect to the receipt page | No, always becomes GET |
| 304 | Not Modified | Cached product data still valid (ETag matched) | No body sent |
| 307 | Temporary Redirect | Request moved to another server during maintenance | Yes |
| 308 | Permanent Redirect | Permanent move that must keep POST as POST | Yes |
Practice: GET /redirect returns 302; POST /redirect?status=307 keeps the POST; POST /redirect?status=303 switches to GET. Conditional GET: request /products/101 with If-None-Match set to its ETag and you get 304. Details in HTTP headers and caching.
4xx Client Error Codes
| Code | Name | Meaning | Real-life example |
|---|---|---|---|
| 400 | Bad Request | Malformed request: bad JSON, invalid parameter format | /users/abc when id must be a number |
| 401 | Unauthorized | Not authenticated: missing, invalid or expired credentials | Session expired in the banking app |
| 403 | Forbidden | Authenticated, but not allowed | Viewer tries to delete a record |
| 404 | Not Found | Resource doesn’t exist (or is hidden from you) | Order id 999 doesn’t exist |
| 405 | Method Not Allowed | Method not supported on this URL | DELETE /products/101 |
| 406 | Not Acceptable | Server can’t produce the format in Accept | Asking for CSV from a JSON-only API |
| 408 | Request Timeout | Client took too long to send the request | Very slow upload |
| 409 | Conflict | Conflicts with current state | Username already taken; seat already booked |
| 410 | Gone | Permanently removed | Retired API version |
| 412 | Precondition Failed | If-Match version is stale | Two agents editing the same ticket |
| 413 | Content Too Large | Body over the limit | Uploading a 50 MB profile photo |
| 415 | Unsupported Media Type | Wrong Content-Type | Sending text/plain to a JSON API |
| 422 | Unprocessable Content | Well-formed but fails validation rules | Age 17 when minimum is 18 |
| 429 | Too Many Requests | Rate limit exceeded | OTP requested 6 times in a minute |
The Confusing Pairs (Interview Favourites)
401 Unauthorized vs 403 Forbidden
A 401 should include a WWW-Authenticate header. Try it on the Basic auth endpoint with no credentials:
https://sqatools.in/wp-json/sqa-api/v1/basic/profilecurl -i \
"https://sqatools.in/wp-json/sqa-api/v1/basic/profile"On the practice API, a missing API key gives 401, a wrong API key gives 403, and a viewer token trying to create gives 403.
400 Bad Request vs 422 Unprocessable Content
- 400: the server can’t even read it. Broken JSON, empty body, a JSON array instead of an object, text where a number is required in the URL.
- 422: the JSON is valid, but the values break business rules: invalid email, age out of range, unknown enum value.
https://sqatools.in/wp-json/sqa-api/v1/validate/usercurl -i \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"A","email":"not-an-email","age":17}' \
"https://sqatools.in/wp-json/sqa-api/v1/validate/user"The response lists every failing field at once, which is the behaviour you want from a well-designed API.
404 Not Found vs 403 for Hidden Resources
For security, many APIs return 404 instead of 403 when you request another user’s private resource, so attackers can’t even learn that it exists. Both are acceptable; returning the data is not.
409 Conflict vs 412 Precondition Failed
409 is a business conflict (duplicate email). 412 is a technical version conflict when you send If-Match with an old ETag (optimistic locking). The practice API’s /objects supports both scenarios.
5xx Server Error Codes
| Code | Name | Typical cause | What testers do |
|---|---|---|---|
| 500 | Internal Server Error | Unhandled exception, null pointer, bad query | Always a bug; attach request + trace id |
| 501 | Not Implemented | Feature/method not built yet | Check against requirements |
| 502 | Bad Gateway | Gateway got an invalid reply from the service behind it | Check service health and logs |
| 503 | Service Unavailable | Maintenance or overload; should send Retry-After | Verify clients back off and retry |
| 504 | Gateway Timeout | Service behind the gateway too slow | Performance investigation |
Status Codes for Each HTTP Method
| Method | Success | Common errors to test |
|---|---|---|
| GET | 200 (304 if cached) | 400, 401, 403, 404 |
| POST | 201 (200 for actions, 202 async) | 400, 401, 403, 409, 413, 415, 422, 429 |
| PUT | 200 / 201 (upsert) / 204 | 400, 404, 409, 412, 422 |
| PATCH | 200 / 204 | 400, 404, 412, 422 |
| DELETE | 204 / 200 | 401, 403, 404, 409 |
Automating Status Code Checks with Playwright
A data-driven test checks many codes in a few lines. Use maxRedirects: 0 to assert a redirect itself instead of following it.
import { test, expect } from '@playwright/test';
const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';
const cases = [
{ name: 'existing user', path: '/users/1', expected: 200 },
{ name: 'missing user', path: '/users/999', expected: 404 },
{ name: 'non-numeric id', path: '/users/abc', expected: 400 },
{ name: 'page size too big', path: '/users?per_page=51', expected: 400 },
{ name: 'no credentials', path: '/basic/profile', expected: 401 },
{ name: 'service down', path: '/status/503', expected: 503 },
];
for (const c of cases) {
test(`GET ${c.path} -> ${c.expected} (${c.name})`, async ({ request }) => {
const res = await request.get(`${BASE}${c.path}`);
expect(res.status()).toBe(c.expected);
});
}
test('redirect is 302 when not followed', async ({ request }) => {
const res = await request.get(`${BASE}/redirect`, { maxRedirects: 0 });
expect(res.status()).toBe(302);
expect(res.headers()['location']).toBeTruthy();
});
test('503 tells the client when to retry', async ({ request }) => {
const res = await request.get(`${BASE}/status/503`);
expect(res.headers()['retry-after']).toBe('10');
});
Note: res.ok() is true only for 200-299. Playwright does not throw on 4xx/5xx by default, which is what you want for negative tests.
How to Report a Wrong Status Code
A good bug title: “POST /orders returns 500 instead of 422 when quantity is negative”. Include the full request (cURL), the actual and expected status, the response body, the trace or request id header and the environment. Developers fix these fast when they can replay the request.
Interview Questions: HTTP Status Codes
What is the difference between 401 and 403?
401 Unauthorized means the request is not authenticated: credentials are missing, invalid or expired, and the client should log in. 403 Forbidden means the client is authenticated but does not have permission for this action or resource.
What is the difference between 400 and 422?
400 Bad Request means the server cannot parse or understand the request, such as malformed JSON or an invalid parameter format. 422 Unprocessable Content means the request is well-formed but fails business validation, such as an invalid email or an out-of-range age.
Which status code should a successful POST return?
201 Created when a new resource is created, with a Location header pointing to it. 200 OK is acceptable for POST actions that don’t create a resource (login, search), and 202 Accepted for asynchronous processing.
What is the difference between 200 and 204?
Both mean success. 200 OK returns a body; 204 No Content returns no body, commonly used for DELETE or updates where nothing needs to be returned.
What is the difference between 301 and 302?
301 is a permanent redirect: clients and search engines should update to the new URL. 302 is temporary: keep using the original URL. Use 307/308 when the method and body must be preserved.
When does an API return 304 Not Modified?
When the client sends a conditional request (If-None-Match with an ETag or If-Modified-Since) and the resource has not changed. The server returns 304 with no body, and the client uses its cached copy.
What is the difference between 502, 503 and 504?
502 Bad Gateway: the gateway received an invalid response from the upstream service. 503 Service Unavailable: the service is down or overloaded, ideally with Retry-After. 504 Gateway Timeout: the upstream service did not respond in time.
Is a 500 error always a bug?
From a tester’s point of view, yes. A 500 means an unhandled server failure. If it is caused by client input, it should have been a 4xx validation error; if it is caused by an internal problem, it needs fixing and should not expose stack traces.
What status code is returned when rate limits are exceeded?
429 Too Many Requests, ideally with a Retry-After header and rate limit headers such as X-RateLimit-Limit and X-RateLimit-Remaining.
An API returns 200 with an error message in the body. What do you do?
Log it as a defect: the status code should reflect the result (for example 400, 401 or 422). Explain the impact: monitoring misses failures, clients treat errors as success and caches may store failed responses.
What is 409 Conflict used for?
When a request conflicts with the current state of the resource, such as registering an email that already exists, booking a seat that was just taken or deleting a record that other data depends on.