Interactive resource

The API Testing Checklist.

A living checklist for testing any API thoroughly. Tick items as you go and your progress saves automatically. Add your own, filter by area, and export when you're done.

Know your way around APIs already? Skip to the checklist ↓
0%complete
0checked
56remaining
56total checks

Start here

What you are actually testing

A checklist is only useful if you know what each line means in a real request. So before the boxes, here is the whole thing in plain parts. If you have tested APIs for years, skim to the console. If you have not, this is the part that makes the rest click.

An API request, taken apart

Every call is the same handful of pieces. Here is one read request, labelled:

GEThttps://api.irie-eats.com/v1/orders/482?status=paid
Method

What you want to do. GET reads, POST creates, PATCH/PUT change, DELETE removes. Test that each endpoint only accepts the methods it should.

Base URL

Where the API lives. The host and version. A change here (v1 to v2) is a whole compatibility question on its own.

Path

Which kind of thing. /orders/ says you are dealing with orders.

Path parameter

Which exact one. The 482 is the order number. Change it and you are asking for a different record. Changing it to an id you should not see is one of your best tests.

Query string

Options and filters. Everything after the ?. Here, “only paid orders”. This is where filtering, sorting, and pagination live, and where edge cases hide.

Two more pieces do not show in the URL. They each get their own section because each is its own source of bugs.

Headers

Information about the request, sent alongside it. Authorization proves who you are, Content-Type says what format the body is in, Accept says what format you want back. Most “why is this 401” mysteries are a missing or wrong header.

Authorization: Bearer eyJhbGc…
Content-Type: application/json
Body

The data you send, on a POST, PUT, or PATCH. Usually JSON. This is where most real bugs live: missing fields, wrong types, and values the client should never be allowed to set, like a price.

{
  "item": "jerk-chicken-plate",
  "qty": 2
}

What comes back, and the four things to check every time

When you send a request you get a response. “Did it work” is never one yes-or-no. It is four specific questions:

1

The status code

The three-digit verdict. The single fastest signal that something is right or wrong.

201 Created
2

The body

The data returned. Check the fields exist, the types are right, and the values are what you expect, not just that something came back.

{
  "id": 907,
  "total": 1700,
  "status": "paid"
}
3

The headers

Content-Type, caching, rate-limit counters. Right format, no sensitive data leaking out.

Content-Type: application/json
Cache-Control: no-store
4

The time

Fast enough under load, and it fails cleanly when something downstream is slow.

Time: 142 ms

Reading status codes

You do not memorise all of them. You learn the families, then the few that come up daily.

2xxIt worked200 OK, 201 Created, 204 No Content
3xxGo elsewhere301 Moved, 304 Not Modified
4xxYour request was wrong400, 401, 403, 404, 409, 422, 429
5xxThe server broke500, 502, 503, 504

The one distinction people miss: 401 vs 403. 401 Unauthorized means “I do not know who you are” — no token, or a bad one. 403 Forbidden means “I know exactly who you are, and you still cannot have this”. Authentication is the door; authorization is the room. Most access bugs are a 403 that should have happened and did not.

Try it: a REST API console

This is a safe, pretend API for a Jamaican food-delivery app. Nothing leaves your browser. Pick a request, read how it is built, press Send, and watch the status and body. The point is the groups: the same call with one thing changed, so you can feel why the result changes. That is the habit that turns a checklist line into a real test.

GET

Request headers
Request body
 
Press Send to see the response.

A different shape

One more kind: GraphQL

Everything above is REST: many addresses, one per resource, and the HTTP status code tells you what happened. GraphQL is a different shape, and it quietly changes what you test. The same ideas still apply (auth, inputs, edge cases), but the mechanics move.

One address, always POST

You do not walk a set of URLs. Every query is sent to a single endpoint, usually /graphql, and you ask for exactly the fields you want, no more and no less.

Errors hide inside a 200

This is the big one. A GraphQL call usually returns 200 OK even when it failed. The real outcome lives in an errors array in the body. “Assert the status is 200” tells you very little here, so you have to read the body.

Permission is per field

Because the caller picks the fields, the access check sits on each field, not on the endpoint. Ask for a field you should not see and confirm you are refused, even inside an otherwise allowed query.

A query can be a weapon

A deeply nested query can ask the server to do enormous work. Test that depth and complexity limits exist, or a single request can take the whole service down.

Partial data is normal

One response can carry some data and some errors at the same time. Check that your code handles both together, not just all-or-nothing.

Introspection

The whole schema can be queried back out. Confirm introspection is turned off in production if your team means it to be hidden.

Try it: a GraphQL console

Same idea as the console above, in GraphQL’s shape. One endpoint, and every request is a POST. Pick a query, press Send, and watch the status barely move while the real story slides between data and errors.

GET

Request headers
Request body
 
Press Send to see the response.

Where you meet APIs

The same API turns up in a lot of places

An API is not one spot. The same endpoint gets called from a web page, a phone app, other services, your test suite, and live in production. So as a tester you run into API behaviour in different places, and which place depends on where you sit. Each one shows a different kind of bug. Here is where you actually see it, and how you catch a failure in each.

On the web: the Network tab

Your browser already records every call a page makes. Open developer tools, go to the Network tab, and filter to Fetch/XHR to cut the noise. Now click through the app. Each row is a real request you can open up:

  • Status and timing at a glance. A red 4xx or 5xx, or a call that dragged on for seconds, jumps right out.
  • Headers, Payload, Preview. Exactly what was sent and what came back, with JSON shown as a tidy tree.
  • Copy as cURL. Replay the failing call in a terminal or an API client, with its headers and body intact, so you can poke at it.
  • Throttle to Slow 3G or Offline. Force a slow or failed network and watch whether the screen recovers or just freezes.
  • Save as HAR. Hand the developer the exact request and response as evidence, instead of a screenshot of a spinner.

This is the everyday move for web API testing: drive the UI, watch the calls underneath, and when a screen misbehaves, point straight at the request that caused it.

Elements · Network · Console
AllFetch/XHRDocImg
Name
orders/482
menu
orders/483
pay
profile
HeadersPayloadPreviewResponse
General
Request URLhttps://api.irie-eats.com/v1/orders/483
Request MethodGET
Status Code 403 Forbidden
Remote Address34.120.88.7:443
Response headers
content-typeapplication/json
dateTue, 23 Jun 2026 15:30:35 GMT
Click the failing call and the Headers tab shows its Status Code, here a 403 the screen never surfaced.

In an API client

When you want to poke at an endpoint directly, with no interface in the way, you reach for an API client: Postman, Insomnia, Bruno, or Hoppscotch, or plain curl in a terminal. You build the call by hand, hit Send, and read the reply. It is the fastest place to explore a new endpoint, and to replay a failing one you grabbed from the Network tab. Bruno and Hoppscotch are the offline, Git-friendly ones teams are reaching for now.

GET orders/482 + No Environment ▾
GET https://api.irie-eats.com/v1/orders/482 Send
ParamsHeaders 5Body
BodyHeaders 7 200 OK58 ms412 B
PrettyRawJSON ▾
1{
2 "id": 482,
3 "item": "Jerk chicken plate",
4 "qty": 2,
5 "total": 1700,
6 "currency": "JMD",
7 "status": "paid"
8}
Build the call by hand, hit Send, and read the response. This is Postman.

A client is not just for one-off pokes. Its real power is the Tests tab: assertions you write once that run on every Send, and across the whole collection in CI. The first few are table stakes. The one that earns its keep is the business rule, where the real bugs hide. This is close to what my own Postman suites check, here on a pretend combo deal:

pm.test("Status is 200", () =>
  pm.response.to.have.status(200));

pm.test("Responds under 1s", () =>
  pm.expect(pm.response.responseTime).to.be.below(1000));

pm.test("Body has the orders array", () =>
  pm.expect(pm.response.json()).to.have.property("orders"));

// the one that actually matters: the business rule
pm.test("Combo deal: 2 plates apply 10% off", () => {
  const o = pm.response.json();
  pm.expect(o.discountPercent).to.equal(10);
});

On mobile: a proxy

A phone app does not show its calls in a browser, so you put a tool in the middle to watch them. A proxy like Charles Proxy, Proxyman, or mitmproxy sits between the device and the internet. Point the phone’s Wi-Fi proxy at your computer, install the proxy’s certificate on the device so it can read HTTPS, and every call the app makes becomes visible: which endpoint failed, its status, its body, its timing.

Two things trip people up here. From Android 7 onward, apps only trust system certificates by default, so a user-installed certificate may not be enough on its own. And an app using certificate pinning will refuse the proxy outright, so you often need a debug build to see inside it. I go deeper on testing the network on a real device in the mobile app lifecycle and networks article, and on real-device testing overall in the real-device checklist for QA.

Proxyman · Irie Eats (iPhone)
CodeMethodURLDuration
200GETapi.irie-eats.com/v1/orders/48258 ms
200GETapi.irie-eats.com/v1/menu41 ms
500POSTapi.irie-eats.com/v1/pay1.24 s
ResponseRequestHeadersTiming
500 Internal Server Error  ·  POST /v1/pay
{
  "error": "Payment processor timed out"
}
On the phone the order screen looked fine. In the proxy, the POST /v1/pay shows a 500, so the charge actually failed.

Further out: code, contracts, and production

Watching the traffic by hand is only the start. The same endpoint is also tested automatically, between services, against a mock, and in production, and each spot catches a different kind of bug:

Automated, in code

Playwright’s request API, REST Assured, or Karate for end-to-end checks; Schemathesis to throw generated inputs at an OpenAPI spec and find the edge cases you would not have written by hand.

const res = await request.get('/orders/482')
expect(res.status()).toBe(200)

As a contract between services

Consumer-driven contracts with Pact catch a breaking change before two services ever meet. Powerful, and still under-used: most teams skip it and find the break in production instead.

provider.addInteraction({
  withRequest: { path: '/orders/482' },
  willRespondWith: { status: 200 },
})

Against a mock

When the backend is not built yet, a mock from the spec (Prism, WireMock, MSW) lets you test the client now and keeps the front end moving.

prism mock openapi.yaml
# /orders/482 now answers from the spec

In CI and in production

The same checks run on every build, and synthetic calls plus good logs and traces keep watching the live API after release.

# CI, on every push
npm run test:api
# Prod, every minute
GET /health → 200, under 300 ms

No single place shows you the whole API. The skill is knowing which one answers the question in front of you: explore in a client, catch UI breaks in the network, lock changes down with a contract, and keep an eye on the live service in production.

And you will not work at all of these, which is fine. Where you sit decides which places are yours. A backend or microservices team building an API for, say, an e-commerce platform lives in the automated, contract, and CI layers, and may never open a screen. A mobile or web tester usually works the other way, through the app, watching the network for the call that broke a page. On a small team, one person touches all of it. None of these layers is the “real” one. The trick is knowing which your role owns, and which someone else already has covered.

Now run the checklist

You have seen the parts and watched a few calls behave. Here is the full checklist to run against a real API. Tick items as you go, filter by area, add your own, and export it as a record when you are done.

Documentation & Planning

0/6
  • Make a list of the APIs or endpoints that need testing You cannot test what you have not written down. Start with the actual list of addresses you have to cover.
  • Check that each API has a clear purpose, examples, parameters, and responses If the docs cannot tell you what good looks like, you have no way to tell when it is broken.
  • Check that authentication, authorization, rate limits, and errors are documented Know the rules before you test them: who gets in, who is blocked, how often you can call, and what errors to expect.
  • Check the user journey or workflow that depends on this API An endpoint is usually one step in a bigger flow. Follow the real path a user takes through it, end to end.
  • Check whether any privacy, security, or industry rules apply Some data (health, payments, personal info) carries legal rules. Find out which apply before you go poking.
  • Decide which checks should be manual, automated, or supported by an API testing tool Not everything needs a script. Decide what a human checks once and what a machine should check forever.

Request Inputs

0/6
  • Check that the right HTTP method and URL are used for each endpoint GET to read, POST to create. The wrong verb, or a small typo in the address, is a common silent bug.
  • Check that required headers such as Content-Type and Authorization are handled correctly The envelope matters. The wrong format label or a missing token, and the request fails before it even starts.
  • Try valid, invalid, and missing path or query parameters Send a good id, a junk id, and no id at all. A safe API handles all three without falling over.
  • Check that required body fields cannot be missing Leave out a required field. The API should say exactly what is missing, not crash or save half a record.
  • Check that wrong field names, wrong types, extra fields, and incomplete bodies are handled safely Send a number where a word goes, a typo in a field name, an extra field it never asked for. It should reject that cleanly, not choke on it.
  • For a browser-facing API, check CORS: allowed origins and the preflight A web API called from another site needs the right CORS headers and a correct OPTIONS preflight, or the browser blocks the real call before it is even sent.

Response & Status

0/6
  • Check that successful requests return the right 2xx status A create should be 201, a plain read 200. The number should match what actually happened.
  • Check that failed requests return the right 4xx or 5xx status A failure must look like a failure. A 200 with an error message hidden inside is a trap for everyone downstream.
  • Check that the response body has the expected fields, types, and values Do not stop at the status code. Open the body and confirm the right data, in the right shape, is actually there.
  • Check that response headers such as Content-Type are correct The reply should say what it is. JSON labelled as plain text confuses every client that tries to read it.
  • Check that response time stays within the agreed limit The right answer, too slow, is still a fail. Hold it to the speed the team agreed on.
  • Check caching and conditional requests (ETag, 304 Not Modified) If the API caches, a repeat request with If-None-Match should come back 304 with no body, not the whole payload again. Stale or wrong caching quietly serves old data.

Authentication & Authorization

0/5
  • Check that protected APIs reject requests without a token No token should mean no entry: a 401. Try every protected endpoint with the token removed.
  • Check that expired or invalid tokens are rejected An old or tampered token must be refused, never quietly accepted.
  • Check that token refresh or login expiry behaves as expected Sessions should end when they are meant to, and refresh when they are meant to. Test both edges.
  • Check that users cannot access another user's data Log in as one user and ask for another user's record by id. The answer must be no (403). This single test catches real privacy leaks.
  • Check that admin-only actions are blocked for regular users Hiding a button is not security. Call the admin-only address as a normal user and confirm it is still blocked.

Security Checks

0/5
  • Try unsafe inputs such as scripts, SQL-like text, or special characters Paste in a script tag or a quote-heavy string. The API should treat it as plain text, never run it.
  • Try changing IDs, prices, roles, or other values the user should not control Set your price to 1, your role to admin, the id to someone else’s. The server must ignore or reject every one.
  • Check that unsupported methods and content types are rejected Send a DELETE where only GET is allowed, or XML where it wants JSON. It should say no, not try to guess.
  • Check that sensitive data is not returned in responses Read the body closely. Password hashes, full card numbers, and internal ids should never come back.
  • Check that sensitive data is not exposed in error messages or logs Errors should help, not leak. No stack traces, secrets, or raw queries spilled into the message.

Data & Edge Cases

0/5
  • Check empty values, very long values, and special characters Send nothing, send a novel, send emoji and accents. The edges are where things quietly break.
  • Check zero, negative, minimum, and maximum numbers Try 0, -1, and a huge number where a quantity goes. Confirm each is accepted or refused on purpose, not by accident.
  • Check pagination, sorting, and filtering Ask for page 99, sort by a field that does not exist, filter down to nothing. The list should stay sensible.
  • Check duplicate requests Send the same create twice. Do you get two orders, or a clean rejection? Decide which is right, then test for it.
  • Check optional, nullable, and default values Leave the optional fields out. The API should fill in its defaults, not break or invent wild values.

Workflow & State

0/5
  • Check that create requests actually create the record After a create, go and fetch it. A “201 Created” is not the whole story if a follow-up GET cannot find it.
  • Check that update requests only change the intended fields Change one field, then check the rest stayed exactly as they were. Updates love to touch things you did not ask about.
  • Check that delete or archive requests behave as expected After a delete, the record should be gone, or marked archived. Confirm it, do not assume it.
  • Check that failed requests do not leave partial changes behind If a request fails halfway, nothing should be saved. Half a record is worse than none at all.
  • Check that repeated PUT, PATCH, or DELETE requests are safe where they should be Sending the same update twice should land in the same place, not double-charge or double-delete.

Reliability & Limits

0/5
  • Check rate limiting or too-many-requests behavior Hammer the endpoint. It should slow you down with a clear 429, not fall over.
  • Check that a 429 tells the client how long to wait (Retry-After) A rate limit with no Retry-After header leaves a well-behaved client guessing. The honest answer says exactly when to try again.
  • Check what happens when another service is slow or unavailable When something it depends on is down, it should fail gracefully, not hang forever waiting.
  • Check that timeouts are handled cleanly A request that takes too long should give up with a clear message, not leave the user staring at a spinner.
  • Check that error messages are clear enough to understand Could the next person fix the problem from the message alone? If not, the error is too vague.

Watching & Reproducing

0/4
  • Watch the real network traffic, not just the screen Open the browser Network tab, or a proxy on mobile, and read the actual calls. A UI glitch is often a 500 underneath that the screen never showed you.
  • On mobile, capture the app’s calls through a proxy Charles Proxy, Proxyman, or mitmproxy with its certificate installed shows what a phone app really sends. Mind the Android 7 system-cert rule and certificate pinning.
  • Save a HAR or copy the request as cURL so a failure can be replayed Hand the developer the exact request and response, not a screenshot of a spinner. A failure you can replay is a failure that gets fixed.
  • Force a slow, offline, or flaky connection and watch the client Throttle to Slow 3G or drop the network mid-request. The app should show a clear error and recover, not freeze or quietly carry on as if the call succeeded.

Contracts & Compatibility

0/5
  • Check that the API still matches the documentation or schema The docs are a promise. When the real response drifts from them, someone’s code is about to break.
  • Check that field names have not changed unexpectedly Renaming “total” to “amount” quietly breaks every client reading “total”. Catch it before they do.
  • Check that field types are still the same A number that turns into a string overnight breaks the code that did the maths on it. Watch the types.
  • Check that older clients would still understand the response New does not give you the right to break old. Confirm an older app can still read the reply.
  • Check that deprecated fields or versions are handled clearly When you retire a field, warn first and remove later. No silent disappearances.

GraphQL (if your API uses it)

0/4
  • Treat a 200 with an errors array as a failure GraphQL usually answers 200 even when it failed; the real outcome is in the errors array. Assert on errors and the data shape, never on the status code alone.
  • Check authorization on each field, not just the endpoint The caller picks the fields, so ask for one you should not see and confirm it comes back null with an error, even inside an otherwise allowed query.
  • Confirm a query depth or complexity limit exists A deeply nested query can take a service down. A safe server rejects an abusive query before it runs.
  • Confirm introspection is disabled in production if it should be Introspection hands out a full map of your schema. Fine in development, often meant to be off in production.