What is API Testing? How to Test a REST API
API testing checks that an application programming interface returns the right data, status codes and headers for every request it is given — valid ones, invalid ones and malicious ones — and that it does so reliably and quickly. Because APIs sit between the user interface and the data, a bug found here is usually cheaper to fix and more dangerous to miss than one found in a screen.
This guide explains the types of API tests, the anatomy of a request and response, a repeatable method for testing a REST endpoint, the failures that show up most often, and how to do all of it from a browser without installing anything.
What API testing covers
"API testing" is a family of checks rather than one activity. Most teams run the first three on every change and the rest before a release.
- Functional testing — does each endpoint return the correct response for valid input? Does a POST create the record, does a GET return it, does a DELETE remove it?
- Validation and negative testing — what happens with missing fields, wrong types, values out of range, malformed JSON, an expired token or a request that is too large? The API should answer with a clear 4xx error, never a 500.
- Contract testing — does the response still match the agreed schema (OpenAPI, JSON Schema)? Removed fields and changed types break every consumer.
- Authentication and authorization — are protected endpoints rejecting anonymous requests (401) and requests from the wrong user or role (403)?
- Performance — how long does a request take, how large is the payload, and how does the endpoint behave under concurrent load?
- Security — headers, injection in query parameters and bodies, rate limiting, and whether error responses leak stack traces or internals.
Anatomy of a request and a response
Every HTTP exchange has the same parts, and every one of them is something to assert on.
- Method — GET reads, POST creates, PUT replaces, PATCH updates part of a resource, DELETE removes. Using the wrong one is a bug in itself.
- URL and query string — the resource path (/orders/42) plus filters and paging (?status=paid&page=2).
- Request headers — Content-Type tells the server what you are sending, Accept what you expect back, Authorization who you are.
- Request body — usually JSON for REST; must match the documented schema.
- Status code — 2xx success, 3xx redirect, 4xx the client did something wrong, 5xx the server failed. The exact code matters: 201 for created, 204 for no content, 400 for bad input, 404 for not found, 409 for conflicts, 422 for validation errors.
- Response headers — Content-Type, caching, rate-limit counters, CORS and security headers.
- Response body — the data, or a consistent error object with a machine-readable code and a human-readable message.
Request
Part 1:POST Part 2:/api/v1/orders HTTP/1.1 Part 3:Host: api.example.com Content-Type: application/json Authorization: Bearer eyJhbGciOi... Part 4:{"customerId": 7, "items": [{"sku": "A-100", "qty": 2}]}
Response
HTTP/1.1 Part 5:201 Created Part 6:Content-Type: application/json Location: /api/v1/orders/9021 Part 7:{"id": 9021, "status": "pending", "total": 249.90}
- 1Method
POSTcreates; using the wrong method is a bug in itself. - 2URL and query stringThe resource path, plus filters and paging, as in:
/orders/42?status=paid&page=2 - 3Request headers
Content-Typesays what you are sending,Acceptwhat you expect back,Authorizationwho you are. - 4Request bodyUsually JSON for REST; must match the documented schema.
- 5Status codeThe exact code matters:
201for created,400for bad input,422for validation errors. - 6Response headers
Content-TypeandLocationhere; also caching, rate-limit counters, CORS and security headers. - 7Response bodyThe data, or a consistent error object with a machine-readable code and a human-readable message.
Text version
POST /api/v1/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
{"customerId": 7, "items": [{"sku": "A-100", "qty": 2}]}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/orders/9021
{"id": 9021, "status": "pending", "total": 249.90}How to test a REST API, step by step
- Read the contract first. Open the OpenAPI/Swagger document or the API docs and list every endpoint, its parameters, the success response and every documented error.
- Send the happy path. For each endpoint, send a valid request and assert the status code, the response schema and the key values. Save the response — it is your baseline.
- Break the input on purpose. Remove required fields, send the wrong type, exceed lengths, use boundary values (0, -1, maximum + 1), send empty and very large bodies, and send malformed JSON. Expect precise 4xx codes and error messages that name the problem.
- Test authentication. Call protected endpoints with no token, an expired token, a token for another user and a token with the wrong scope. Expect 401 or 403, never data.
- Check state changes. After a POST, GET the resource and confirm it exists; after a DELETE, confirm it is gone; after a PUT, confirm every field changed and nothing else did.
- Assert on headers, not just bodies. Content-Type must be application/json, caching must be correct for private data, CORS must be right for browser clients.
- Measure. Record the response time of each call; anything that trends upward between releases is a regression even if it still "works".
- Automate what you repeated. Turn the requests and assertions into a collection or a script so they run on every build.
Try it: REST API Tester Try it: API Endpoint Tester Try it: API Authentication Tester
What to assert in every response
- Status code is exactly the documented one.
- Content-Type header is correct and the body parses.
- The body matches the schema: required fields present, types correct, no unexpected fields for strict contracts.
- Values are right: IDs, totals, dates in ISO 8601, enumerations within the allowed set.
- Errors are consistent: same shape on every endpoint, a stable machine code, no stack traces or SQL in the message.
- Response time is within the agreed budget.
Failures that show up most often
- 200 OK with an error inside the body — clients cannot tell success from failure without parsing text.
- 500 for bad input instead of 400/422 — the validation layer is missing or crashes.
- Different error shapes on different endpoints — every consumer needs custom handling.
- Silent contract changes — a field renamed or a type changed without a version bump.
- Missing authorization checks — the token is valid but belongs to a user who should not see the resource.
- Inconsistent pagination — page sizes, offsets and totals that disagree between endpoints.
- Time zone bugs — timestamps without an offset, or dates that shift by a day.
Try it: API Status Code Simulator Try it: Response Contract Comparator
Testing webhooks and callbacks
Webhooks reverse the direction: the API calls you. To test one you need a URL that accepts the call and shows you exactly what arrived — headers, signature, body and timing — so you can check the payload against the documentation and verify the signature with the shared secret before writing a single line of handler code.
API testing tools: desktop apps vs browser tools
Postman, Insomnia and Bruno are excellent for large, long-lived collections. For day-to-day work — checking an endpoint, reproducing a bug, validating a response, capturing a webhook — a browser tool is faster because there is nothing to install or sign in to. The Mutqan API tools cover that daily loop: build and send requests, validate responses against rules or a schema, simulate status codes, capture webhooks and generate test cases straight from an OpenAPI file.
Frequently asked questions
What is the difference between API testing and unit testing?
Unit tests exercise a function in isolation inside the codebase. API tests send real HTTP requests to a running service and check the response, so they cover routing, serialization, validation, authentication and the database together.
Can I test an API without Postman?
Yes. Any tool that can send an HTTP request and show the response works — cURL on the command line, a browser-based request builder, or a script. The important part is what you assert, not the client.
Which status code should a validation error return?
400 Bad Request is the general answer; 422 Unprocessable Content (formerly Unprocessable Entity) is widely used when the JSON is well-formed but the values fail validation. Whichever you choose, use it consistently across the API.
How do I test an API that requires authentication?
Obtain a token the way a real client would (login endpoint, OAuth flow or an API key), send it in the Authorization header, and also test the negative cases: no token, expired token, wrong scope, another user's token.
What is contract testing?
Checking that an API's actual responses match its published contract — usually an OpenAPI document or JSON Schema — so that a change that would break consumers is caught before release.
How many test cases does an endpoint need?
At minimum one happy path, one for each documented error, one authentication failure and the boundary values of each parameter. A tool can generate that baseline from the OpenAPI spec; a tester then adds business-specific scenarios.
References
- RFC 9110: HTTP SemanticsThe meaning of HTTP methods, header fields, status codes and content negotiation, independent of the wire version.
- OpenAPI SpecificationThe standard for describing REST APIs: paths, operations, parameters, schemas, responses and security schemes.
- OWASP API Security Top 10 (2023)The ten most common API weaknesses: broken object-level authorization, broken authentication, unrestricted resource consumption and more.
- Postman DocsOfficial documentation for the API client used to send requests, write test scripts and run collections.
Practice exercise
Test the returns endpoint of an online store
You test the REST API of a fictional online electronics store on staging. The developer has finished POST /api/v1/orders/{orderId}/returns, which lets a customer request a return and a refund for items of a delivered order, and has checked only the happy path. The contract excerpt, the synthetic test data, the developer’s request and the results of three more checks from a first run are below.
Apply the guide’s step-by-step method to this one endpoint. Write the test set with one line per test: what you change in the developer’s valid request, the expected status code and error code, and anything else to assert. Change one thing per test, so that a failure points to one cause. Then judge the three results of the first run, and write down what the contract leaves open as questions instead of guessing.
Pen and paper are enough; allow about 45 minutes. If you like, check your quantity and comment-length values with the Boundary Value Analysis tool, take more invalid inputs for a field from the Negative Test Generator, and look up what a status code means in RFC 9110 with the HTTP Status Code Lookup.
Fictional online electronics store - Returns API on staging. Today: Thu 1 Oct 2026.
Every response is JSON (Content-Type: application/json) with Cache-Control: no-store.
Every error has the same shape ("field" is sent with VALIDATION_ERROR only):
{"error": {"code": "VALIDATION_ERROR", "message": "...", "field": "items[0].qty"}}
POST /api/v1/orders/{orderId}/returns Request a return of items from an order
Auth Bearer token of the order's customer, with the scope returns:write
Body items required, at least one entry: {"sku": string, "qty": integer}
each SKU once, and only SKUs of this order
qty: 1 to the returnable quantity of the SKU
(quantity delivered minus quantity already in returns)
reason required: DAMAGED | WRONG_ITEM | NOT_AS_DESCRIBED | CHANGED_MIND
comment optional text, at most 500 characters
id, status, refundAmount and createdAt are set by the server;
if the client sends them, they are ignored.
refundAmount = the sum of qty x unit price of the returned items.
Rule The order must be delivered, and the return requested within 14 days
of delivery.
201 Created the return; header Location: /api/v1/returns/{id}
400 MALFORMED_JSON the body is not valid JSON
401 UNAUTHENTICATED no token, or the token is invalid or expired
403 FORBIDDEN the order belongs to another customer
403 INSUFFICIENT_SCOPE the token does not have the scope returns:write
404 ORDER_NOT_FOUND no order with this id
409 ORDER_NOT_RETURNABLE the order is not delivered, or the 14 days have passed
415 UNSUPPORTED_MEDIA_TYPE Content-Type is not application/json
422 VALIDATION_ERROR a field is missing or invalid; "field" names it
Target response time on staging: under 800 ms.
GET /api/v1/orders/{orderId}/returns The returns of an order (same auth rules)
Test data (synthetic; restored before each test, after which no returns exist)
CUS-101 TOKEN_A (returns:write), TOKEN_A_EXPIRED, TOKEN_A_READ (orders:read only)
CUS-202 TOKEN_B (returns:write)
O-5001 CUS-101 delivered 24 Sep 2026 A-100 x 2 at 49.95, B-200 x 1 at 120.00
O-5002 CUS-101 shipped, not delivered A-100 x 1 at 49.95
O-5003 CUS-101 delivered 10 Sep 2026 A-100 x 1 at 49.95
O-6001 CUS-202 delivered 28 Sep 2026 C-300 x 1 at 75.00
The developer's happy-path check
POST /api/v1/orders/O-5001/returns HTTP/1.1
Host: staging-api.example.com
Content-Type: application/json
Authorization: Bearer TOKEN_A
{"items": [{"sku": "A-100", "qty": 1}], "reason": "DAMAGED"}
HTTP/1.1 201 Created
Content-Type: application/json
Cache-Control: no-store
Location: /api/v1/returns/R-9001
{"id": "R-9001", "orderId": "O-5001", "status": "requested",
"items": [{"sku": "A-100", "qty": 1}], "reason": "DAMAGED", "comment": null,
"refundAmount": 49.95, "createdAt": "2026-10-01T10:15:00+03:00"}
First run of three more checks (each on restored data)
R1 O-5001, TOKEN_A, items [{"sku": "A-100", "qty": 3}], reason DAMAGED
-> 500 Internal Server Error
{"error": "IllegalStateException at ReturnService.java:88"}
R2 O-5001, TOKEN_B, items [{"sku": "A-100", "qty": 1}], reason DAMAGED
-> 201 Created, Location: /api/v1/returns/R-9002, body with "orderId": "O-5001"
R3 O-5001, TOKEN_A, items [{"sku": "A-100", "qty": 1}], reason OTHER
-> 200 OK
{"success": false, "message": "Invalid reason"}- Read the contract first: list every documented response and every rule that has an edge, and check whether the test data lets you test each edge.
- Happy path: make the developer’s request your first test, with everything to assert in its response, and add a second valid request that returns everything in the order.
- Break the input on purpose: the body format, each field, each boundary and the order states in the rule. For each test, give the expected status code, error code and
field. - Authentication: no token, an expired token, another customer’s token and a token with the wrong scope.
- State changes and headers: what you check after a request with a separate
GET, and the headers every response must carry. Add how you will measure and automate the set. - Judge R1 to R3: pass or fail, which of the guide’s common failures it is, and the expected response. Decide which one you report first, then list your questions for the product owner.
Show the model answer
Step 1 — read the contract. It documents nine outcomes: 201, 400, 401, 403 FORBIDDEN, 403 INSUFFICIENT_SCOPE, 404, 409, 415 and 422, so the set needs at least nine tests before any boundary. The rules with edges are qty (from 1 to the returnable quantity: 2 for A-100 and 1 for B-200 in O-5001), comment (500 characters), items (at least one entry, each SKU once, only SKUs of the order) and the 14-day window. The test data has no order on the edge of the window, so ask for two more synthetic orders of CUS-101, for example O-5004 delivered on 17 Sep 2026 (14 days before today) and O-5005 delivered on 16 Sep 2026 (15 days), each with A-100 × 1, so that the developer’s request fits them unchanged. Run every test on restored data: each successful return lowers the returnable quantity for the next test. The model set below numbers the tests RT-01 to RT-25; your numbering and wording may differ.
Step 2 — happy path. RT-01 The developer’s request (A-100 × 1, DAMAGED): 201; Location is /api/v1/returns/ followed by the id of the body; Content-Type: application/json and Cache-Control: no-store; the body has every field of a return with the right type — refundAmount a number, not a string, comment null, createdAt in ISO 8601 with an offset — and the right values: orderId O-5001, status requested, items and reason as sent, refundAmount 49.95; response time under 800 ms. Save this response as your baseline. RT-02 Everything in the order: A-100 × 2 and B-200 × 1, reason WRONG_ITEM, with a short comment: 201, both items listed, the comment returned as sent, and refundAmount 219.90 (2 × 49.95 + 120.00), a value that checking the status code alone would never catch.
Step 3 — break the input; each test is RT-01 with one change. RT-03 Body cut off after {"items": [ → 400 MALFORMED_JSON. RT-04 The valid body sent with Content-Type: text/plain → 415 UNSUPPORTED_MEDIA_TYPE. RT-05 items left out → 422 VALIDATION_ERROR, field items. RT-06 "items": [] → 422, field items. RT-07 The boundaries of qty for A-100, whose returnable quantity is 2 (2-value boundary value analysis: each boundary value and its closest neighbour in the adjacent partition): 0 → 422 with field items[0].qty; 1 → 201 (RT-01); 2 → 201; 3 → 422. RT-08 Invalid values and types of qty: -1, 1.5 and "1" (a string) → 422 each. RT-09 SKU C-300, which belongs to another order → 422, field items[0].sku. RT-10 A-100 twice in items → 422. RT-11 reason left out, and reason OTHER → 422, field reason. RT-12 A comment of exactly 500 characters → 201, the comment returned in full; 501 characters → 422, field comment.
RT-13 Order O-9999 → 404 ORDER_NOT_FOUND. RT-14 O-5002, which is only shipped → 409 ORDER_NOT_RETURNABLE, the response the contract gives for an order that is not delivered (see Q4). RT-15 The window: O-5004 (day 14) → 201 if day 14 counts (Q1); O-5005 (day 15) and O-5003 (day 21) → 409. RT-16 A comment with quotes and angle brackets, such as O'Brien "test" <b>x</b> → 201, and the comment comes back exactly as sent, with no 500 and no SQL or stack trace in any message. Send such inputs only to systems you are authorised to test — here, your team’s staging.
Step 4 — authentication. RT-17 No Authorization header → 401 UNAUTHENTICATED and no return data; RFC 9110 also requires every 401 response to carry a WWW-Authenticate header. RT-18 TOKEN_A_EXPIRED → 401 UNAUTHENTICATED. RT-19 TOKEN_B of customer CUS-202 on O-5001 → 403 FORBIDDEN, with nothing about O-5001 in the body; afterwards GET /api/v1/orders/O-5001/returns with TOKEN_A shows no new return, and the same GET with TOKEN_B → 403. RFC 9110 lets a server answer 404 instead, to hide that the order exists; this contract chose 403, so 403 is the expected result. RT-20 TOKEN_A_READ, which only has orders:read → 403 INSUFFICIENT_SCOPE.
Step 5 — state changes and headers. RT-21 After RT-01, GET /api/v1/orders/O-5001/returns → 200 with exactly one return whose id, items, reason and refundAmount match the 201 body. RT-22 The returnable quantity goes down: after RT-01, A-100 with qty 2 → 422 (only 1 is left), qty 1 → 201, then qty 1 again → 422. RT-23 RT-01 plus "status": "approved", "refundAmount": 999 → 201 with status requested and refundAmount 49.95: a client must not be able to set its own refund (the OWASP API Security Top 10 2023 covers this under API3, Broken Object Property Level Authorization). RT-24 Rejected requests change nothing: after RT-07 with qty 3, RT-11 and RT-14, the list of returns of that order is unchanged. RT-25 The RT-02 request sent twice at the same moment → one 201 and one 422; two 201 responses would refund the same items twice. On every response, errors included, assert Content-Type: application/json, Cache-Control: no-store (the data is private), the contract’s error shape, and Location on 201 only.
The last two steps of the method apply as well: record the response time of every test and flag anything above 800 ms even when the response is correct, and compare the times between builds: a time that keeps rising is a regression even while it stays under 800 ms. Then turn the set into a collection that restores the test data, runs on every build and compares RT-01 with the saved baseline. That is 25 tests for one endpoint. The guide’s FAQ names the minimum — one happy path, one test for each documented error, an authentication failure and the boundary values of each parameter — and RT-21 to RT-25 are what a set built only from single responses misses.
Step 6 — the first run. R1 fails on three counts, all named in the guide: 500 for bad input instead of 422 and an error shape that differs from the contract (a string instead of the error object with code, message and field) are two of its common failures, and the message leaks internals (an exception name, a source file and a line number), which the guide’s list of what to assert in every response rules out. Expected: 422 VALIDATION_ERROR with field items[0].qty.
R2 fails and is the most serious: a missing authorization check — the token is valid but belongs to a customer who has no right to this order. Any signed-in customer could request returns, and so refunds, on someone else’s order, and the response shows that order’s data. The OWASP API Security Top 10 2023 lists this as API1, Broken Object Level Authorization. Expected: 403 FORBIDDEN, and no return created. Report it first, with severity Critical, the exact request with TOKEN_B, the 201 response and the return R-9002 it created; after the fix, run RT-19 again as a confirmation test, the GET with TOKEN_B included. R3 fails: 200 OK with an error inside the body, so a client that trusts the status code tells the customer the return was accepted, and the body has yet another error shape. Expected: 422 VALIDATION_ERROR with field reason, and no return created.
Questions for the product owner and the developer. Q1 Does "within 14 days of delivery" include day 14, and is it counted in calendar days in the store’s time zone or in 24-hour periods from the time of delivery? Q2 How are the 500 characters of comment counted for Arabic text and emoji, and is an empty comment accepted? Q3 Is reason case-sensitive, that is, is damaged rejected? Q4 When a request breaks two rules, which error comes first? Examples: another customer’s order with an invalid body, or O-5002, which is not delivered, so none of its quantity is returnable yet (409 or 422?). The usual expectation is the authorization check, so that validation messages reveal nothing about someone else’s order.
Check your own work: every documented status code should appear in your set at least once; each negative test should change exactly one thing in RT-01; and each expected result should name the status code and the error code, not just "an error". In ISTQB terms, the contract is the test basis, each documented response and each rule is a test condition, and RT-07, RT-12 and RT-15 apply 2-value boundary value analysis. If your set had no checks after the response, RT-21 to RT-25, add them: a response can look right while the data behind it is wrong.
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.
Part of the QA Roadmap
Before this topic
Next topics
Tools mentioned in this guide
Compose and send HTTP requests (method, URL, headers, auth, body) and inspect the response — a lightweight Postman in your browser.
Test REST endpoints with full control over method, headers and JSON body, then assert on status, headers and JSON paths.
Quickly check that an API endpoint is reachable: status, response time, size, content type and TLS.
Validate an API response against expectations: status, required headers, JSON schema and field rules.
Validate API responses against a JSON Schema or an OpenAPI response schema.
Get a public endpoint that returns any HTTP status code, with optional delay and custom JSON body, to test client error handling.
Get a unique URL, send webhooks to it and inspect method, headers, body and timing in real time.
Generate a structured test-case matrix (positive, negative, auth, boundary) from an OpenAPI / Swagger specification.
Test Bearer, Basic, API-key and custom-header authentication against an endpoint and compare authorised vs unauthorised responses.
Measure an endpoint's response time over several samples: min, max, average, median and p95.