Asynchronous search — built for batch and high-volume work
The batch endpoint. Submit the search, get a job id back immediately, and collect the result when it is ready — poll GET /v1/searches/{searchId} or supply a webhookUrl and we POST the finished envelope to it.
Because you are not holding a connection open there is no request-timeout ceiling on the work, so every named jurisdiction runs to completion. Use it for bulk screening, periodic re-screens of a roster you already hold, backfills, unattended scheduled jobs, and broad national sweeps.
Use 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 read the same corpus at the same freshness tier and return the same envelope; the choice is about how you collect the result, not how current it is. To submit many searches at once, POST /v1/batch takes up to 1,000 queries in a single request.
Request
- Base URL: https://api.offendersearch.app
- URL: https://api.offendersearch.app/v1/searches
- Auth: API key in header X-API-Key
Request body
Jurisdiction codes — a registry id, or any two-letter code that registry COVERS, in any case. null/omit = the full dataset. ★ AN UNRECOGNISED VALUE IS A 422 (changed 2026-08-05), never an empty result. ["XX"] and ["TEXAS"] previously returned status: "complete", records: 0, sourcesQueried: 0, warnings: [] — indistinguishable from "this person is not a registered offender" — for one wrong token, while ["tx"] returned 1,219. GET /v1/sources lists every id.
OPT-IN ONLY. If true AND query.state is set, run only the registries that COVER that state instead of all 58. query.state alone does NOT turn this on — it is a residence FILTER, not a registry selector, and inferring the scope from it (the behaviour until 2026-08-05) hid every registrant whose registry is not the state they live in: 7.0% of records, and 36% of a matched out-of-state-residence sample returned ZERO. When this narrows the fan-out the response reports counts.sourcesSkippedByScope and a "NARROWED SEARCH" warning, because a registry that was never asked contributes a 0 meaning UNKNOWN, not NO MATCH.
Record types to search on THIS endpoint, which searches the sex-offender registries only. Criminal records are not a recordType here — they are a separate live product at POST /v1/criminal/search, because they carry a different record shape (no photograph or tier, and a recordType of their own naming jail / prison / court / warrant). ★ ANY OTHER VALUE IS A 422 (changed 2026-08-05) — note the underscore: ["sexoffender"] used to return a clean, complete zero.
RETIRED — accept-and-ignore for one release. The per-request daily/weekly freshness tier is gone: it was a two-value price knob that returned the SAME data either way, and it carried a +$0.01/call daily surcharge that no longer exists. SO now serves ONE cached, continuously-updated answer (advertised "close to live"), and re-verification is requested via the live block below instead. Any value you send here — a former tier or anything else — is ACCEPTED with no 422 and simply not read: no billing tier, no serving window, and it is no longer echoed back in the response. Remove it at your convenience.
DEPRECATED 2026-08-04 — leave it unset. The default (include) is the behaviour you want. Still accepted so that request bodies written against the old default keep working. Note before sending it: omit is a filter that DROPS WHOLE REGISTRIES from your own search. A registry it drops contributes zero records and is reported honestly as incomplete — sourceStatus[].incompleteReason: "excluded", counted in counts.sourcesIncomplete, and named in the INCOMPLETE SEARCH: warning — so a 0 from it means UNKNOWN, not NO MATCH. Remove the parameter and those records come back. Does NOT affect billing. (The omit window is now the widest window we serve — the per-request freshness tier it once compared against is retired.)
Fuzzy-match tolerance applied uniformly across ALL jurisdictions. ★ AN UNRECOGNISED VALUE IS A 422 (changed 2026-08-05). A typo used to fall through to balanced: "strick" returned 16 records where the strict the caller asked for returns 11 — a silent WIDENING, and the same fallthrough narrows in the other direction. strict=exact; balanced=nicknames/variants/initials/typos(dist 1) on first name, exact last; broad=also prefix/typo on last name. (query.prefixMatch/query.fuzzy override this — see Query.)
★ THE COMPLETE, ENUMERATED LIST OF WHAT EACH MODE DOES TO A NAME. Nothing outside this table is applied to a name in any mode. Every transformation is reported per record in matchDetail.strategies, so you can always see which one produced a given hit — and filter on it. GIVEN NAME (firstName) exact — equal, case- and accent-insensitive. strict, balanced, broad nickname — a curated given-name equivalence table (123 groups, 418 names): Bob/Bobby/Rob/Robert, Tony/Anthony, Peggy/Margaret, Bill/Will/William. It is a fixed list, not an algorithm; the equivalents we hold for any record are published on that record as nicknames, so you can read the table straight off the wire. balanced, broad initial — one letter against a full name, either direction: J matches John, and J on file is matched by John. balanced, broad prefix — the stored name STARTS WITH what you sent, minimum 3 characters: Thom -> Thomas, Marcel -> Marcellous. Anchored at the start, never a substring. balanced, broad fuzzy — Damerau-Levenshtein edit distance (a swap of two adjacent letters counts as ONE edit, so Jhon -> John). BUDGET: 1 edit in balanced, 2 in broad. Not phonetic — see the broad warning below for why the second edit is opt-in. balanced (1), broad (2) middle — the given name you sent is the person's MIDDLE name. 81.1% of the records we hold carry one, and many people are known by theirs, so firstName=Wayne returns the man registered as Dean Wayne Ellison. WHOLE TOKEN, EXACTLY, minimum 2 characters: Wayne matches the Wayne of Wayne Allen; it does not match Waynette, Wayn or the initial W. Never prefixed, never fuzzy: some names are overwhelmingly MIDDLE names (Lee is 20x commoner in the middle slot than the first), so for exactly the names this matches it draws on a much larger pool — which is where spelling latitude is least affordable. Reported as firstName: "middle" and capped at 0.60. balanced, broad absent — the record publishes no given name at all, so your query could not be DISPROVED. Kept and flagged; never a confirmation. every mode
SURNAME (lastName) exact — equal, or equal to one whole token of a compound surname (Smith matches Hamilton-Smith, and never Goldsmith). strict, balanced, broad prefix — starts-with, minimum 3 characters. broad only fuzzy — edit distance, budget SCALED BY LENGTH: 0 edits at <=4 characters, 1 at 5-7, 2 at >=8. Hernandez -> Fernandez matches; Ross -> Rose does not. broad only There is no nickname table for surnames, and asking for one (nameMatch.lastName: ["nickname"]) is a 422.
ALIASES — searched in balanced and broad, not in strict, and with exact/prefix/fuzzy only. Aliases arrive unstructured and in mixed word order, so every token is tested and we cannot tell you which token was the given name — which is why nickname and initial are not offered on them. An alias-only hit is reported as alias:<strategy> and caps matchConfidence at 0.55. ★ WHAT WE DO NOT DO, STATED PLAINLY, BECAUSE IT IS THE QUESTION EVERY EVALUATION ASKS. No phonetic or Soundex matching (Smyth -> Smith works because it is one edit; Schmidt -> Smith does not and will not). No transliteration. No middle-name field. No cross-field matching — a firstName query is never tested against a surname. No inference of one name from another beyond the fixed nickname table above. If you need a variant we do not cover, send it as a second query; we would rather you saw two honest answers than one widened one.
★ CORRECTION, 2026-08-05 — balanced USED TO FUZZ THE GIVEN NAME AT 2 EDITS, WHICH IS NOT WHAT THIS DOCUMENT HAS EVER SAID. Every client-facing surface promised edit distance 1 for balanced; the engine ran 2. Measured on 4,000 production records over 12 surnames and 25 given names, the second edit returned 2,504 records where 1 returns 1,274 — and of the 1,230 it added, ZERO were nickname-table equivalents and 98.0% carried an entirely unrelated given name (juan->john, carl->paul, gary->mark, rick->eric). Two edits on a four-letter name replace half of it. balanced now behaves as documented. If you were relying on the wider behaviour, ask for broad — it is the same budget, named, and its cost is documented below. Recall was measured before the change on a control cohort: 21 of 21 people from a customer's own audit corpus, and 40 of 40 blind registrants queried by their registered given name, are all still returned.
★ PREFER balanced (the default), ESPECIALLY FOR A NATIONAL SEARCH. balanced already handles the variants people reach for broad to solve — nicknames (Tony/Anthony), initials, first-name prefixes and first-name typos. Measured against 34,894 competitor records, balanced matched 98.28% and broad recovered only 38 more (0.11%).
★ broad FUZZES THE SURNAME, AND THAT COSTS PRECISION. Its surname edit-distance budget scales with length (0 edits at <=4 characters, 1 at 5-7, 2 at >=8), which is why Hernandez/Fernandez matches but Ross/Rose does not. Even so, broad returns roughly 13.5% surname false positives by design: at 6 letters a single edit cannot separate Siegel from Riegel any more than Smith from Smyth. Every such hit is labelled lastName:fuzzy in matchBasis — filter on it before acting. In this API a false positive (naming an innocent person as a registered offender) is a materially worse error than a miss.
★ broad MAY NOT FINISH AT NATIONAL SCOPE. It cannot use the indexed surname range, so it reads far more candidate rows per registry; a 57-registry fan-out can exceed the per-registry deadline. When that happens the response says so (status: partial, counts.sourcesIncomplete > 0, a warnings[] sentence naming each registry) — it is never silently short. Production 2026-08-04, {"firstName":"Tony","lastName":"Wilson"}: balanced returned 182 records complete; broad returned 32 with sourcesIncomplete: 29. Scope broad with query.state to use it safely.
Parity: request extensive detail. ["stateData"] (or ["extensive"]) adds the stateData block. It is the ONLY field behind this option — offenses[] and images[] come back on every response. A response reports what it left out in top-level omittedFields.
Maximum time to wait, in ms. Default 2 minutes (120000); lower it for a faster, bounded response. ★ BOUNDED TO 1000-600000 SINCE 2026-08-05 — outside that is a 422. 0 and -1 were previously accepted and silently ignored, and on the cache-served sync path the per-registry probe deadline governs anyway, so a sub-second value could never have meant what it said.
On deadline: partial=return what completed (status=partial); error=return 504. ★ Any other value is a 422 (changed 2026-08-05); it used to fall through to partial.
Optional URL to notify when the async search completes.
Example request
{
"query": {
"firstName": "John",
"lastName": "Smith",
"dob": "1985-06-14",
"age": 39,
"city": "Chicago",
"state": "IL",
"zipcode": "60614",
"lat": 41.9,
"lng": -87.65
},
"jurisdictions": [
"IL",
"IN"
],
"live": {
"jurisdictions": [
"TX"
]
}
}Response
Search accepted; poll resultsUrl for status + results.
How many jurisdictions this search will query.
Poll this for status + results.
Example response
{
"searchId": "srch_9f2c1a7b",
"estimatedCompletionSec": 20,
"resultsUrl": "/v1/searches/srch_9f2c1a7b"
}Changes
No recorded changes to this operation across all 1 revision of this API.