Offendersearch
API Reference · v1.0.0

Async search, webhooks & idempotency

Submit and collect, with no request-timeout ceiling. Signed webhooks and idempotent retries.

Base URL https://api.offendersearch.app

Choosing sync or async

Use POST /v1/search when a person is waiting on the answer — it is the interactive endpoint and returns in one round trip within deadlineMs, with per-jurisdiction status in sourceStatus. Use POST /v1/searches for batch and high-volume work: submit the search, then collect the result by polling GET /v1/searches/{searchId} or by supplying a webhookUrl. Both read the same corpus and return the same envelope — the choice is about how you collect the result, not how current it is. A live re-verification of more than two sources is collected here the same way.

SynchronousAsynchronous
EndpointPOST /v1/searchPOST /v1/searches
ShapeOne round trip; elapsedMs on every responseSubmit now, collect when ready
ScopeBounded by deadlineMsNo request-timeout ceiling — every named jurisdiction runs to completion
DataIdentical — same corpus and same response envelope
DeliveryIn the responsePoll or webhook
Best forInteractive checks, anything user-facingBulk screening, periodic re-screens, backfills, scheduled jobs

For many independent lookups in a single HTTP call — a roster, a CSV export — see Batch & CSV search, which is the third shape and returns one result per input row.

POST/v1/searchesAuth: X-API-Key

Asynchronous search — built for batch and high volume

The batch endpoint. Submit the search, get a job id back immediately, and collect the result when it is ready. Built for bulk screening, periodic re-screens, backfills and unattended scheduled jobs.

The async endpoint accepts every field the synchronous search accepts, plus an optional webhookUrl. Instead of waiting, it returns 202 Accepted with a searchId and a resultsUrl right away and keeps working in the background until every named jurisdiction has responded.

Because you are not holding a connection open, there is no request-timeout ceiling on the work — which is what makes this the right endpoint for bulk screening a roster, re-screening a population on a schedule, backfills, and broad national sweeps where you want every jurisdiction rather than a fast answer.

Retrieve results by polling GET /v1/searches/{searchId} until status is complete, or supply a webhookUrl and we will POST the finished SearchResponse to it — no polling required.

Reach for POST /v1/search instead when a person is waiting on the answer: that is the interactive endpoint and it returns in one round trip. Both endpoints read the same corpus and return the same envelope — the choice is about how you collect the result, not how current it is. A live re-verification of more than two sources is collected here the same way.

Send an Idempotency-Key header to make retries safe: a repeated key returns the original job instead of starting (and billing) a second search. See Idempotency.

What you can do
  • Built for volume. No request-timeout ceiling, so every named jurisdiction runs to completion. Submit many, collect the results as they finish.
  • Webhook or poll. Get notified via webhookUrl, or poll the results URL on your own schedule.
  • Same query surface. All query fields, jurisdictions, match, live, and include carry over.
  • Safe retries. An Idempotency-Key header collapses duplicate submissions to one job and one bill.
Headers
FieldTypeDescription
X-API-Key
required
stringYour secret API key.
Idempotency-Key
optional
stringOptional. A unique client token; retries with the same value return the original job.
Body
FieldTypeDescription
…SearchRequest
required
objectEvery field from POST /v1/search is accepted (query, jurisdictions, match, live, include, …).
webhookUrl
optional
uriWe POST the completed SearchResponse here when the search finishes.
Request
curl https://api.offendersearch.app/v1/searches \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f0c2e14-order-4821" \
  -d '{
    "query": { "lastName": "Doe" },
    "jurisdictions": null,
    "webhookUrl": "https://acme.example/hooks/os"
  }'
Response
200 OK
{
  "searchId": "srch_9f2a7c",
  "status": "running",
  "estimatedCompletionSec": 45,
  "resultsUrl": "https://api.offendersearch.app/v1/searches/srch_9f2a7c"
}

Returns 202 Accepted. status is "pending" or "running"; poll resultsUrl or wait for the webhook.

Webhooks

Supply a webhookUrl on an async search and the finished SearchResponse is POSTed to it when the search completes — no polling required. Delivery is a single best-effort attempt with a 10-second timeout: it is not retried and not signed. Treat it as a notification, then confirm the result by fetching GET /v1/searches/{searchId} with your API key — that response is the source of truth, and polling it always works even if the webhook never arrives.

Delivery
POST to your webhookUrl
{
  "searchId": "srch_9f2a7c",
  "status": "complete",
  "counts": { "records": 3, "sourcesQueried": 58,
              "sourcesComplete": 58, "sourcesIncomplete": 0 },
  "sourceStatus": [ /* one entry per jurisdiction */ ],
  "records": [ /* … scored, de-duplicated records … */ ]
}
Confirming the result
Express handler
app.post("/hooks/os", express.json(), async (req, res) => {
  res.sendStatus(200);             // acknowledge promptly

  // The webhook is a notification: it is unsigned and sent once.
  // Fetch the search with YOUR key and trust that response instead.
  const { searchId } = req.body;
  const r = await fetch(
    `https://api.offendersearch.app/v1/searches/${searchId}`,
    { headers: { "X-API-Key": process.env.OS_API_KEY } });
  if (r.ok) enqueue(searchId, await r.json());   // de-duplicate on searchId
});
Async-search webhooks are unsigned and sent once, so never act on the payload alone: fetch GET /v1/searches/{searchId} with your API key to confirm, and keep polling as the fallback. (Monitoring alerts are different — those webhooks are signed; see the Monitoring API.)

Idempotency

Network retries should never start — or bill — a second search. Send an Idempotency-Key header (any unique client-generated string, e.g. a UUID or your order id) on POST /v1/searches. If that key has been seen before, the original job is returned instead of a new one being created, so a dropped-connection retry is safe.

Safe retry
curl https://api.offendersearch.app/v1/searches \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Idempotency-Key: 7f0c2e14-order-4821" \
  -H "Content-Type: application/json" \
  -d '{ "query": { "lastName": "Doe" } }'

Reuse the same key when retrying the same logical request; use a fresh key for a genuinely new search. Keys are scoped to your account and retained long enough to cover normal retry windows. For 429 handling and backoff, see Errors & rate limits.