Sex Offender Search API · v1.0.0

Retrieving individual records

A search returns records; this is how you get one of them back later, which identifier to use, and what each identifier is for.

When to use this

A reviewer opens a hit from yesterday's queue; an audit asks what a cited record said; a reconciliation job checks that the person you flagged is still the same person. Each needs one record by identifier rather than a new search. GET /v1/records/{recordId} resolves a record by either identifier a search published for it — our recordId or the registry's own uuid — and is not billed. You will build the lookup, decide which identifier to store for which purpose, and handle the two non-200 answers.

Before you start

  • A test key (API key only on this route). Run a sandbox search for Exact Match first; the record carries recordId rec_28db9639d712c4c61e8e, personRef prs_28db9639d712c4c61e8e and uuid SBX-OH-100001.
  • Three identifiers, three jobs: `personRef` (prs_ + 20 hex) is the stable key for the person and what a clearance names; `recordId` (rec_ + 20 hex) is the handle for this result and what this route and a report cite; `uuid` is the registry's own number, unique only together with its jurisdiction.
  • A rec_ id resolves while the search that returned it is retained — 14 days — and from the platform's record index; a registry uuid resolves from the current data, and a superseded uuid still resolves to the person's current record.
The one thing people get wrong. Do not key a person on recordId. It describes the merge that produced the record for that query, so the same person can carry a different recordId on a scoped search and on a full search. Key the person on personRef; keep recordId to cite a specific result.

Step by step

  1. Search, and keep three identifiers. From the Exact Match record store personRef, recordId and uuid with the jurisdiction (sources[].jurisdiction).
  2. Fetch by `recordId`. GET /v1/records/rec_28db9639d712c4c61e8e. The full Record comes back; the match fields are empty because nothing was matched.
  3. Fetch by registry `uuid`. GET /v1/records/SBX-OH-100001 returns the same record. In production a uuid shared by several jurisdictions answers 409.
  4. Handle `404` and `409`. A 404 means this identifier was never issued for a record, or the search that issued it has aged out — never that the person is unregistered. A 409 means the uuid is ambiguous; use recordId.

Steps 2–3 — the lookups

curl -s https://sandbox.offendersearch.app/v1/records/rec_28db9639d712c4c61e8e -H "X-API-Key: $OFFENDERSEARCH_KEY"
curl -s https://sandbox.offendersearch.app/v1/records/SBX-OH-100001          -H "X-API-Key: $OFFENDERSEARCH_KEY"
GET /v1/records/rec_28db9639d712c4c61e8e → 200 (trimmed)
{
  "recordId": "rec_28db9639d712c4c61e8e",
  "personRef": "prs_28db9639d712c4c61e8e",
  "uuid": "SBX-OH-100001",
  "recordType": "sex_offender",
  "matchConfidence": null,
  "matchBasis": [],
  "matchDetail": { "strategies": {}, "fieldsPresent": [], "signals": [], "matchedNameType": null, "verification": null },
  "matchedName": null,
  "name": { "first": "Exact", "middle": "Sample", "last": "Match", "suffix": "", "full": "Exact Sample Match" },
  "dob": "1980-01-01", "birthYear": 1980, "dobPrecision": "exact", "age": "46",
  "addresses": [ { "type": "residence", "line1": "100 Sample St", "city": "Sampleville", "county": "Sample", "state": "OH", "zipcode": "00000", "lat": null, "lng": null } ],
  "registrationState": "OH", "addressStates": ["OH"],
  "offenses": [ { "crime": "Sexual battery", "statute": "SAMPLE 2907.03", "tier": "Tier III", "convictionDate": "2004-05-12" } ],
  "stateData": { "status": "Active", "designation": "Tier III sex offender", "registrationDuration": "Lifetime",
                 "isLifetimeRegistration": true, "complianceStatus": "compliant", "lastVerificationDate": "2026-09-01",
                 "lawAgency": "Sample County Sheriff's Office" },
  "flags": { "absconder": false, "predator": false },
  "images": [ { "url": "https://sandbox.offendersearch.app/v1/sandbox/photos/1.svg" } ],
  "sources": [ { "jurisdiction": "OH", "registryName": "Sandbox sample registry (OH)", "recordUrl": "https://offendersearch.app/docs/sandbox", "lastCheckedAt": "2026-10-01T00:00:00+00:00" } ],
  "matchState": null, "unverified": false,
  "sandbox": true
}

Two things differ from the search copy. The match fields are empty (matchConfidence: null, matchBasis: [], matchState: null): a fetch matches nothing, so there is nothing to explain. And stateData is populated here exactly as on a search — the full record is the default on both.

Step 4 — the two refusals

{ "detail": "Not found. A recordId resolves while the search that returned it is retained; run the search again to get a current recordId, or look the person up by registry uuid and state." }

Registries re-issue their own numbers; one uuid can identify a registrant in Florida, another in Pennsylvania and a third in Wisconsin. The route returns one record and will not guess, so the 409 sends you to recordId, which is namespaced per jurisdiction and does not collide. The sandbox's uuids are unique, so you will not see a 409 there.

Reading the result

IdentifierShapeStore it forNever use it for
personRefprs_ + 20 hexRecognising the same person next time; clearances; monitors. Stable across re-issued registry numbers.Citing a specific result.
recordIdrec_ + 20 hexCiting this result in a report, dispute or audit line; GET /v1/records/{recordId}.Keying a person; it differs between query scopes.
uuid + sources[].jurisdictionregistry's own string, may be ""Cross-referencing the registry's own listing; a superseded one still resolves.Lookups without the jurisdiction — it collides across states.
sources[].recordUrlURL or ""A link for a reviewer to verify at the source.De-duplicating or diffing.

Every top-level key is present on every record; "" means the registry does not publish the field, null means unknown or not applicable, [] means none. The full 37-key list and the empty-value rules are in the Record object.

Edge cases and pitfalls

  • `404` is about the identifier, not the person. After 14 days a rec_ id from a search can stop resolving; run the search again for a current one. Never show "not registered" on a 404.
  • A `rec_` copy can be light. The record a rec_ id returns may be the copy a previous search served, without stateData when that search did not ask for it. Read stateData as "not requested" when null on a fetch of a light copy.
  • Ambiguity is real in production: tens of thousands of registry numbers are shared across jurisdictions. Prefer recordId for lookups you automate.
  • Control characters and ids over 256 characters answer `404`, never a 5xx.
  • To re-open the whole search, not one record, use GET /v1/searches/{searchId}/results (stored records, 14 days, up to 200).
  • Photos. images[].url on some registries points at the platform's media route, which also needs your key.

Production checklist

  • personRef is the primary key for people in your own tables; recordId and searchId are stored per decision.
  • Lookups by uuid always carry the jurisdiction in your own data and tolerate a 409.
  • 404 renders as "record no longer available", never as a clearance.
  • Re-fetches happen within the 14-day window, or a new search is run.

Product overview