Sex Offender Search API · v1.0.0

Evidence and supporting documents

The registry's own website, printed to one PDF per registry behind a cover page with a hash. Asynchronous, polled, and downloadable for 30 days.

When to use this

A verification report says what the platform found. Evidence shows what the registry itself displayed at that moment: every result page and each matching registrant's detail page, printed as shown, behind a cover page carrying the capture time and a SHA-256 of the document. Compliance teams ask for it when a decision may be questioned later. You will build the flow: send live.evidence: "pdf" with the registries to print, poll the job, read evidence[], download each PDF and store it with its hash.

Before you start

  • A test key. On the sandbox the documents are fixed SAMPLE PDFs, the first poll is already complete, and billed is false.
  • The request is an ordinary POST /v1/search with a live block that has evidence: "pdf" and `jurisdictions` naming at most 20 registries. live.scope is refused for evidence (422), and query.lastName is required.
  • Price: $0.25 per completed document, at most $5.00 per search, on top of the live search's own charge ($0.02 per completed source). An incomplete document bills nothing (pricing).
The one thing people get wrong. An evidence search always answers 202, never a SearchResponse. The searchId begins evs_, the records and the documents arrive on GET /v1/searches/{searchId} (the pollUrl), and the download link in each evidence[] entry works for 30 days. Treat the 202 as a job handle and persist it.

Step by step

  1. Submit. Search Exact Match with dob 1980-01-01 and "live": {"jurisdictions": ["OH"], "evidence": "pdf"}. Store the evs_ id from the 202.
  2. Poll. GET the pollUrl every 2–5 seconds. While pending/running the body has zeroed counts and evidence: []; when finished it is the live SearchResponse plus evidence[], usage.liveEvidence, usage.liveEvidenceUsd and sourceSearchId.
  3. Read `evidence[]`. One entry per registry: complete with downloadUrl, sha256, pages and often an inline pdfBase64; or incomplete with an incompleteReason and billed: false.
  4. Download and file. GET /v1/searches/{evs_id}/evidence/{CODE}.pdf returns the PDF (application/pdf); verify the SHA-256 and store it with the evs_ id and the sourceSearchId.

Steps 1–4 in code

# 1. submit → 202
curl -s https://sandbox.offendersearch.app/v1/search \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" -H "Content-Type: application/json" \
  -d '{"query": {"firstName": "Exact", "lastName": "Match", "dob": "1980-01-01"},
       "live": {"jurisdictions": ["OH"], "evidence": "pdf"}}'
# 2. poll the pollUrl from the 202
curl -s https://sandbox.offendersearch.app/v1/searches/evs_41dabad43b4cdfd43e663573 -H "X-API-Key: $OFFENDERSEARCH_KEY"
# 4. download one registry's document
curl -s -o evidence-OH.pdf https://sandbox.offendersearch.app/v1/searches/evs_41dabad43b4cdfd43e663573/evidence/OH.pdf \
  -H "X-API-Key: $OFFENDERSEARCH_KEY"
Step 1 — 202 Accepted
{
  "searchId": "evs_41dabad43b4cdfd43e663573",
  "status": "pending",
  "mode": "async",
  "registries": ["OH"],
  "pollUrl": "/v1/searches/evs_41dabad43b4cdfd43e663573",
  "sandbox": true
}
Step 2 — poll while the job runs (production shape)
{
  "searchId": "evs_41dabad43b4cdfd43e663573",
  "status": "running",
  "registries": ["OH"],
  "counts": { "records": 0, "sourcesQueried": 58, "sourcesIncomplete": 0 },
  "records": [],
  "sourceStatus": [],
  "evidence": []
}
Step 2 — poll when finished (trimmed)
{
  "searchId": "evs_41dabad43b4cdfd43e663573",
  "status": "complete",
  "counts": { "records": 1, "recordsReturned": 1, "sourcesQueried": 58, "sourcesComplete": 58, "sourcesIncomplete": 0, "sourcesSkippedByScope": 0 },
  "sourceStatus": [
    { "source": "OH", "status": "complete", "matched": 1, "fromCache": false, "incomplete": false, "incompleteReason": null }
  ],
  "records": [ { "recordId": "rec_28db9639d712c4c61e8e", "personRef": "prs_28db9639d712c4c61e8e", "matchState": "dob_match", "matchConfidence": 0.9 } ],
  "warnings": [],
  "usage": { "liveChecks": 1, "liveChargeUsd": 0.02, "capped": false, "display": "1 live checks · $0.02",
             "liveEvidence": 0, "liveEvidenceUsd": 0.0 },
  "meterEvents": [],
  "sourceSearchId": "srch_42006068f66b3f51abf41f49",
  "evidence": [
    {
      "jurisdiction": "OH",
      "status": "complete",
      "kind": "text",
      "pages": 2, "resultPages": 1, "detailPages": 1,
      "capturedAt": "2026-10-01T12:00:00+00:00",
      "sourceUrl": "https://offendersearch.app/docs/sandbox",
      "pageUrls": [],
      "sha256": "c48fa5f9921ccead4acfec7191378776adc5210c8f0544bd2eab6923a2e47e28",
      "bytes": 2195,
      "downloadUrl": "/v1/searches/evs_41dabad43b4cdfd43e663573/evidence/OH.pdf",
      "billed": false,
      "chargeUsd": 0.0,
      "pdfBase64": "JVBERi0xLjMKJenr…",
      "inline": true
    }
  ],
  "sandbox": true
}

In production a completed document has billed: true, chargeUsd: 0.25, and usage.liveEvidence / usage.liveEvidenceUsd count it (min(n × 0.25, 5.00)). sourceSearchId is the live search on your account that produced records[] — that id, not the evs_ id, is what POST /v1/report and a clearance cite. pdfBase64 is present when the document is 4 MB or smaller (inline: true); above that, use downloadUrl.

An incomplete document (production shape)
{
  "jurisdiction": "NV",
  "status": "incomplete",
  "incompleteReason": "source_unreachable",
  "detail": "the registry website did not answer within the time allowed",
  "billed": false
}
Step 4 — GET /v1/searches/{evs_id}/evidence/OH.pdf
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="offendersearch-evidence-evs_41dabad43b4cdfd43e663573-OH.pdf"

Reading the result

FieldValuesWhat to do
evidence[].statuscomplete, incompleteDownload on complete; read incompleteReason otherwise.
evidence[].incompleteReasonrenderer_unavailable, source_unreachable, timed_outTransient — retry the evidence search for that registry.
blocked, no_results_page, render_failed, query_unsupported, no_recipeNot fixed by retrying: the site refused the visit, showed no results page for this query, could not be printed, cannot search on these fields, or cannot be printed yet. Record the reason.
evidence[].sha256hexVerify the download; the same value is printed on the cover page.
evidence[].kindtext, image, mixedtext is searchable; image is a full-page capture (some registries only work this way).
evidence[].pages / resultPages / detailPagesintegersWhat the document holds, after the cover page.
evidence[].downloadUrlpathGET it with your key within 30 days; 404 afterwards, for another account, or for an incomplete document.
usage.liveEvidence, usage.liveEvidenceUsdinteger, numberBilled documents and the charge, separate from liveChargeUsd.
sourceSearchIdsrch_…The citable search behind the records.

Edge cases and pitfalls

  • `live.scope` is a `422` here: live.scope is not supported for evidence; name the registries in live.jurisdictions. Run the Regular search first, read registrationState from the hits, then request evidence for those registries.
  • Capacity: the evidence service answers 429 with Retry-After: 10 when it is busy. Nothing ran; wait and resubmit.
  • Unavailable: 503 with {"error": {"code": "unavailable", "message": "…"}} — not the usual detail envelope. The search was not run and nothing was billed; retry, or omit live.evidence.
  • A job that does not finish within 15 minutes polls as status: "error" with error: "the job did not finish; nothing was billed for it". Resubmit.
  • Up to 20 registries per search. Split a longer list into several evidence searches; each has its own $5.00 ceiling.
  • The main host's request validation is not applied to an evidence request — an unknown jurisdictions[] code or a match typo in the same body is not refused by it. Validate the body as you would for POST /v1/search before sending.
  • Poll with an API key. GET /v1/searches/{evs_id} on the main host does not accept a dashboard session.
  • 30 days is the document's life, longer than the 14-day search retention and the 7-day report window. Download and store what you need to keep.

Production checklist

  • evs_ id persisted on the 202; poller with a 2–5 s interval and a 15-minute cap.
  • Every complete document downloaded, hash-verified and stored with evs_ id, sourceSearchId, capturedAt and sha256.
  • Every incomplete reason recorded; transient reasons retried once.
  • usage.liveEvidenceUsd reconciled against Dashboard → Usage.

Product overview