Quickstart
Your first authenticated criminal search, and the five checks to read off every response.
Base URL https://api.offendersearch.appAuthenticate
Every request carries your secret key in the X-API-Key header — the same key you already use for the Sex Offender API. Criminal Search is a product on your existing account, not a separate credential. Create one in your dashboard and export it.
export OFFENDERSEARCH_BASE=https://api.offendersearch.app
export OFFENDERSEARCH_KEY="os_live_…"Your first search
query.lastName is required; a nameless query is declined with 422 guard_unbounded_query rather than answered with an arbitrary slice. Note that page and perPage live at the top level of the request, not inside query.
curl -X POST https://api.offendersearch.app/v1/criminal/search \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": { "lastName": "Hamilton", "firstName": "Alex", "state": "TX" },
"page": 1, "perPage": 50
}'The default is the cached answer: it consults no live source, so status is complete and sourceStatus is [].
{
"status": "complete",
"counts": { "records": 12, "recordsReturned": 12,
"sourcesQueried": 0, "sourcesComplete": 0, "sourcesIncomplete": 0 },
"warnings": [],
"page": 1, "perPage": 50, "totalPages": 1,
"sourceStatus": [],
"records": [ "… up to perPage normalized records, each with a matchState …" ],
"searchId": "crs_7f2a1c9e0b4d6a8f1e23",
"legal": { "notice": "Not a consumer report. This information may not be used for any purpose under the Fair Credit Reporting Act (15 U.S.C. § 1681 et seq.)." }
}Read the response, in this order, every time
status—"complete"means the search finished."partial"means at least one live source could not be checked to the end. A cached search is always"complete".counts.sourcesIncomplete—> 0means a live source fell short, so an empty result is a lower bound, not a confirmed absence. Always0on a cached search. See Result completeness.warnings—[]means nothing shortened this answer, sowarnings == []is a valid completeness assertion. Any entry beginsINCOMPLETE SEARCH:and names the sources.counts.records— the total matched, before your page slice.records.length(andcounts.recordsReturned) is only the current page.matchState,matchConfidence,matchBasis, per record — did the date of birth verify, and how did the name match? See Searching by date of birth.
records array genuinely means nobody in the corpus matched. On a live search, read counts.sourcesIncomplete first — a > 0 is not determined, never not found.Get the per-source annex
The light response omits the per-source annex and names it in omittedFields (["sourceData"] by default). Send include: ["sourceData"] to un-omit it. Nothing else is ever held back — the normalized record is fully present on every response.
Where to go next
- Search — every parameter, both modes, and worked examples.
- Matching & confidence — the required surname anchor and the confidence arithmetic.
- Date of birth — how one
dobquery reaches date, year and age evidence. - Coverage today — the public source of truth for what is covered right now.