Pagination & response size
The full result set in one response by default — plus page, perPage and totalPages when you want slices.
Base URL https://api.offendersearch.appThis page as Markdown/docs/pagination.mdPagination
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.
records, and that is the whole result set. 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. Pagination exists for your convenience — a UI that shows twenty rows at a time, or a smaller payload — not as a condition of completeness.| 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 — you do not need to compute the page count. Read page back rather than assuming it: a page past the end is clamped to the last one. Separately, counts.records is the total matched before the page slice, which is not records.length.
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);
// counts.records is the TOTAL before the slice; totalPages is authoritative.
for (let n = 2; n <= first.totalPages; n++) {
const next = await page(n); // page(n).page echoes the page you got
}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+radiusMilesquery can match many registrants. Narrow the radius or add name filters, or run it as an async search, which has no request-timeout ceiling. 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 — pass
pageand readpage/totalPagesfrom the response exactly as before. 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 described in Result completeness is reported through
incompleteReason: "truncated", and it is not something paging can walk through — narrow the query instead.