# Pagination & response size

> Search returns the full de-duplicated result set in one response unless you paginate. page, perPage and totalPages, and how a cap differs from a page.

- **HTML:** https://offendersearch.app/docs/pagination
- **Base URL:** https://api.offendersearch.app
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **All documentation as markdown:** https://offendersearch.app/docs.md

## Pagination

`POST /v1/search` returns the **full, de-duplicated result set in a single response** unless you ask otherwise. Set `query.page` and/or `query.perPage` to paginate; set neither and you get everything at once. Results are totally and stably ordered, so paging never reshuffles.

You do not have to page to get a complete answer. There is no page-until-empty loop to write and no cursor to carry. On a response you did not paginate, `perPage` comes back equal to `counts.records` and `totalPages` is `1` — that is the shape confirming you have everything.

| Parameter | Default | Constraint |
| --- | --- | --- |
| `query.page` | 1 | `>= 1` — `0` returns 422. A page past the end is clamped to the last page. |
| `query.perPage` | 20 *when paginating* | `>= 1` |

The search envelope returns `page`, `perPage` and `totalPages` directly. `counts.records` is the total matched *before* the page slice, which is not `records.length`.

```javascript
async function page(n) {
  const res = await fetch("https://api.offendersearch.app/v1/search", {
    method: "POST",
    headers: {
      "X-API-Key": process.env.OFFENDERSEARCH_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      query: { lastName: "Doe", state: "NJ", page: n, perPage: 20 },
    }),
  });
  return res.json();
}

const first = await page(1);
for (let n = 2; n <= first.totalPages; n++) {
  const next = await page(n);
}
```

## Response size

An unpaginated search for a common surname can reach roughly 20 MB of JSON (gzip is applied above 1 KB). Paginate for anything interactive. An unpaginated response is returned up to a defined **4,000-record** cap and sets `capped: true` when that cap applies; send `perPage` and every matched record is reachable, with `capped` false. Branch on `counts.records` against `counts.recordsReturned`: if they differ, there is more to fetch.

- **Broad geographic searches.** A wide `lat/lng` + `radiusMiles` query can match many registrants. Narrow the radius or add name filters, or run it as an async search. Radius matching selects on published coordinates, so use a name search when you need selection that is independent of address coordinates.
- **Compatibility endpoint.** The compat endpoint preserves the legacy paged envelope. Regular searches page 20 per page there; a GIS (lat + lng) search pages 50.
- **A cap is not a page.** The per-jurisdiction candidate limit is reported through `incompleteReason: "truncated"` and is not something paging can walk through — narrow the query instead.

---

## Related

- Previous: [Result completeness & per-source status](https://offendersearch.app/docs/result-completeness.md)
- Next: [Freshness tiers & source coverage](https://offendersearch.app/docs/freshness.md)
- Index: [Offendersearch API documentation](https://offendersearch.app/docs.md)
