Status codes, pagination and filtering
The status codes an API should return, offset versus cursor pagination, and the query parameters clients expect for filtering, sorting and fields.
Codes clients actually branch on
| Code | Return it when |
|---|---|
200 OK | A successful read or update with a body |
201 Created | A resource was created — include Location |
202 Accepted | Work is queued asynchronously — include a status URL |
204 No Content | Success with nothing to say (DELETE, PUT) |
304 Not Modified | Conditional GET and the ETag still matches |
400 Bad Request | Malformed syntax, failed parameter validation |
401 Unauthorized | Missing or invalid credentials |
403 Forbidden | Authenticated but not allowed |
404 Not Found | No such resource |
409 Conflict | Version or uniqueness conflict |
412 Precondition Failed | If-Match did not match the current ETag |
422 Unprocessable Content | Well-formed but semantically invalid |
429 Too Many Requests | Rate limited — send Retry-After |
A client should be able to decide what to do from the status line alone: retry, re-authenticate, or stop. Returning 200 for a failure forces every consumer to parse the body before it knows whether anything worked.
Offset versus cursor pagination
| Offset / page | Cursor / keyset | |
|---|---|---|
| Request | ?page=3&per_page=50 | ?cursor=eyJpZCI6MTA0Mn0 |
| Stable while data changes | No — rows shift between pages | Yes |
| Random page jumps | Yes | No, forward (and sometimes back) only |
| Deep-page cost | Grows: the database still walks the skipped rows | Constant, with the right index |
| Best for | Admin tables and reports | Feeds, exports, high-volume lists |
GET /orders?status=shipped&per_page=50&page=3 HTTP/1.1
HTTP/1.1 200 OK
Link: <https://api.example.com/orders?status=shipped&per_page=50&page=4>; rel="next",
<https://api.example.com/orders?status=shipped&per_page=50&page=9>; rel="last"
X-Total-Count: 412
{ "data": [ { "id": "1042" } ], "meta": { "page": 3, "per_page": 50, "total": 412 } }GET /orders?status=shipped&limit=50&cursor=eyJpZCI6MTA0Mn0 HTTP/1.1
HTTP/1.1 200 OK
Link: <https://api.example.com/orders?status=shipped&limit=50&cursor=eyJpZCI6MTA5Mn0>; rel="next"
{ "data": [ { "id": "1042" } ], "next_cursor": "eyJpZCI6MTA5Mn0" }⚠️
Encode the cursor so clients do not depend on its contents, and always bind it to the same filter and sort. Reusing a cursor with different parameters silently returns the wrong page.
Filtering, sorting and field selection
GET /orders?status=shipped&created_after=2026-01-01
GET /orders?status=shipped&status=refunded # repeated = OR
GET /orders?sort=-created_at,total # - means descending
GET /orders?fields=id,status,total # sparse fieldsets
GET /orders?include=customer,items # related resources
GET /orders?q=laptop+stand # free-text search- Define AND across different parameters and OR within a repeated one; without a rule, clients guess differently.
- Whitelist sortable fields. An unsortable column should be a 400, not a slow query.
- Put the paging parameters and the filters in the
LinkURLs you emit, so a client can follow them without reconstructing the query. - Cap
per_pageandlimit. A missing limit is how one client takes down a database. - Version the response envelope —
dataplusmeta— rather than returning a bare array, so you can add fields later.
FAQ
Which pagination should a new API use?
Cursor pagination for anything that grows continuously; offset pagination when users need to jump to a page number. Several mature APIs offer both, with the offset variant capped.
Should totals be in the body or a header?
Either works, but counting is often the most expensive part of the query. Emit it only when asked, for example with a
with_total=true parameter.Related
Resource modelling and methods Versioning and error shapes
Last refreshed 2026-09-18.