# Async jobs & webhooks

> When a job will take longer than a request, you get an id. How to poll it, how webhooks differ by product, and how to make retries safe.

- **HTML:** https://offendersearch.app/docs/platform/async-and-webhooks
- **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

## The pattern

1. **Submit.** The endpoint answers with a `searchId` (or the product's id) and the path to poll. The key differs per entry point — `resultsUrl` on `POST /v1/searches` (`202`), `poll` on a Sex Offender live search that went async (`200`, `status: "pending"`) and on a criminal `202`, `pollUrl` on a `live.evidence` `202` — so read the one your entry point documents.
2. **Persist the id immediately.** It is the only handle to the work and to its bill.
3. **Poll.** `GET` the poll path every 2–5 seconds and cap your attempts (a few minutes). Sex Offender: `status` `pending`/`running` while it works, the full envelope when done, `error` if the job failed (nothing billed). Criminal: `202` with a JobRecord (`result: null`) while pending, `200` with the envelope under `result` when complete, `422` if the stored query was refused when run.
4. **Or be told.** Where the product supports it, give a `webhookUrl` and the finished envelope is POSTed to you.

| Product | Async entry | Poll | Webhook |
| --- | --- | --- | --- |
| Sex Offender | `POST /v1/searches` (`202`, `resultsUrl`); `POST /v1/search` with more live sources than run inline (`200`, `status: "pending"`, `poll`); `live.evidence: "pdf"` (`202`, `pollUrl`) | `GET /v1/searches/{searchId}` (API key, not a session) | `webhookUrl` on `POST /v1/searches` only; **not signed** — put a secret token in your URL. [Details](/docs/async-and-webhooks). |
| Criminal | `POST /v1/criminal/search` with more than two live sources (`202`, `poll`) | `GET /v1/criminal/searches/{searchId}` — `202` while pending, `200` with `result` | Poll only. [Details](/docs/criminal/async). |
| Monitoring | Alerts are produced by the daily evaluation | `GET /v1/monitors/{monitorId}/alerts` | Alert deliveries to `channels.webhookUrl`; see [Delivery](/docs/monitoring/delivery) for the signature header and the current signing status. |
| FCRA | Orders progress asynchronously | `GET /v1/fcra/orders/{order_id}` | Signed events to the partner's endpoint; redelivery via `POST /v1/fcra/events/{event_id}/resend`. [Details](/docs/fcra/webhooks). |

## Idempotency

The APIs do not read an idempotency header. Every accepted async submission is a new job, billed at acceptance. Make retries safe on your side: persist the id from the acknowledgement before anything else, and on a dropped connection recover by polling that id instead of submitting again. A `429` or a `5xx` on submission means no job was accepted, so submitting again is safe.

## Receiving a webhook

- Answer `2xx` quickly and do the work after; a slow handler is treated as a failed delivery.
- Treat deliveries as at-least-once: key your processing on the event or search id.
- Where a product signs deliveries, verify the signature on the raw body before trusting it; where it does not (sex offender search `webhookUrl`), authenticate with a secret token in the URL and fetch the result from the API if you need certainty.
