# Requests & responses

> Versioning, pagination, timeouts, identifiers and billing lines — the same on every product.

- **HTML:** https://offendersearch.app/docs/platform/requests
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Docs index for agents:** https://offendersearch.app/llms.txt

## Base path and versions

Every endpoint lives under `/v1`. Changes are additive: new fields and new enum values appear, nothing is renamed or removed. Ignore fields you do not know and treat unknown enum values as "other". A breaking change would ship under a new base path and be announced on the [changelog](/docs/changelog) first.

## Pagination

| Product | How | Notes |
| --- | --- | --- |
| Sex Offender search | Optional `query.page` / `query.perPage` | Send neither and the whole de-duplicated result comes back in one response (capped at 4,000 records, said in `capped`). A search that names nobody (radius, free text) is always paginated. [Details](/docs/pagination). |
| Criminal search | Top-level `page` (≥1) / `perPage` (1–200, default 50) | Offset pagination over the ranked set; `totalPages` on every response. [Details](/docs/criminal/pagination). |
| Monitors, alerts, FCRA lists | Per endpoint | See each product's reference. |

## Timeouts and deadlines

- Set your HTTP client timeout to at least **60 seconds** for a synchronous search with live re-checks; cached searches usually answer in well under a second.
- The Sex Offender search accepts `deadlineMs` and `onDeadline` to bound how long it waits for slow jurisdictions, and reports `elapsedMs` on every response. [Details](/docs/search).
- Anything long-running — a live check of many sources, evidence documents, a criminal job — answers with an id to poll (a `202`, or for a Sex Offender live search that went async a `200` whose `status` is `pending`). See [Async jobs & webhooks](/docs/platform/async-and-webhooks).

## Identifiers

| Identifier | Shape | Use |
| --- | --- | --- |
| `searchId` | `srch_` + hex (sex offender), `crs_` + hex (criminal), `evs_` + hex (live evidence) | Poll an async job, render a report, find it in history. Persist it the moment you receive it. |
| `recordId` / `externalId` | Per product | Fetch one record again: `GET /v1/records/{recordId}`; `GET /v1/criminal/records/{recordId}?source=` where the path value is the criminal record's `externalId` (a criminal record has no `recordId` field). |
| `personRef` | Opaque | Stable handle for a person across searches; what a clearance names. |
| `monitorId`, `alertId`, `orderId`, … | Per product | See each reference. |

## Billing lines on the response

Responses that charge more than the base call carry `meterEvents[]`: one entry per meter, with `kind` (for example `criminal_call`, `live_source`, `pdf`) and `quantity`. A criminal search always carries them; a Sex Offender search carries them only when a `live` block ran (a Regular search is one `call`, counted in [Dashboard → Usage](/dashboard/usage) and not echoed on the response). Log what you receive and reconcile the rest from the dashboard.
