Coverage and data sources
A clear answer is only as good as the sources behind it. The catalog is public, the freshness is per source, and every search says which sources stood behind it.
What you'll build
Two things. A coverage check you can run before you buy and keep running afterwards — which federal, state and county sources the platform holds, which are live today and which are known but not on file yet, and when each was last refreshed. And a per-search reading of the coverage block so your outcome logic can say "clear across N sources, M not on file yet" instead of a bare "clear".
Before you start
GET /v1/criminal/sourcesneeds no key and works on both hosts; it is the public coverage statement and is cached for up to five minutes. On the sandbox it returns the production catalog unchanged.- A test key (
os_test_…) for the search half. Sendquery.stateso thecoverageblock is scoped, andinclude: ["coverage"]to list every source in scope. - The rendered catalog and the human list: Coverage today and All data sources.
health.ageSeconds says. Read the catalog to know a source's currency, not to predict which sources will answer. To get a source's answer as of now, run a live check (Cached vs. live verification).Step by step
- Read the catalog.
GET /v1/criminal/sources.catalogtotals the rows; eachsources[]row has a publicid(the jurisdiction code), aname,covers,scope,recordTypein words,statusand ahealthblock. - Separate live from pending.
status: "live"is on file today.status: "pending"is known and not yet available — it contributes nothing to any search until it turns live. - Read freshness per source.
health.lastSuccessAtandageSecondssay when the stored copy was last refreshed;freshForrestates it as the tiers it falls inside (daily≈ ≤36 h,weekly≈ ≤8 days).[]means never refreshed. - Read coverage on a search. Send
state(andcounty). The envelope'scoverageblock andcounts.sourcesOnFile/sourcesPendingsay what stood behind this answer;coverage.pendingSources[]names what did not.
# the public catalog — no key
curl -s https://sandbox.offendersearch.app/v1/criminal/sources | head -c 1200
# a scoped search with the full in-scope source list
curl -s https://sandbox.offendersearch.app/v1/criminal/search \
-H "X-API-Key: os_test_your_key" -H "Content-Type: application/json" \
-d '{ "query": { "lastName": "Record", "firstName": "Clear", "state": "TX", "county": "Bexar" }, "include": ["coverage"] }'{
"catalog": { "total": 1410, "live": 727, "comingSoon": 683, "states": 50 },
"coverage": { "jurisdictions_total": 1410, "jurisdictions_live_connector": 727, "jurisdictions_pending_connector": 683, "states_covered": 50 },
"sources": [
{ "id": "TX-BEXAR", "name": "Bexar County, TX", "covers": ["TX"], "scope": "county",
"recordType": "County jail roster", "status": "live",
"health": { "lastSuccessAt": "2026-10-24T05:12:00+00:00", "ageSeconds": 30074, "freshFor": ["daily", "weekly"] } },
{ "id": "TX-TRAVIS", "name": "Travis County, TX", "covers": ["TX"], "scope": "county",
"recordType": "Court records", "status": "pending",
"health": { "lastSuccessAt": null, "ageSeconds": null, "freshFor": [] } }
],
"count": 1410
}{
"status": "complete",
"counts": { "records": 0, "recordsReturned": 0, "sourcesQueried": 0, "sourcesComplete": 0, "sourcesIncomplete": 0,
"candidatesCapped": false, "exclusionsIncluded": false, "sourcesOnFile": 4, "sourcesPending": 1 },
"coverage": {
"scope": { "state": "TX", "county": "Bexar", "countyRecognized": true },
"sourcesOnFile": 4,
"sourcesPending": 1,
"pendingSources": [ { "code": "TX-COURT", "name": "Texas — statewide court records", "recordType": "Court records", "scope": "state" } ],
"sources": [
{ "code": "TX-BEXAR", "name": "Bexar County, TX", "recordType": "County jail roster", "scope": "county", "status": "live" },
{ "code": "TX-DOC", "name": "Texas — statewide prison system", "recordType": "Prison system", "scope": "state", "status": "live" },
{ "code": "US-WARRANT", "name": "Federal — Warrant system", "recordType": "Warrant system", "scope": "federal", "status": "live" },
{ "code": "US-EXCLUSION", "name": "Federal — Exclusion list", "recordType": "Exclusion list", "scope": "federal", "status": "live" },
{ "code": "TX-COURT", "name": "Texas — statewide court records", "recordType": "Court records", "scope": "state", "status": "pending" }
]
},
"records": []
}Read the two together: zero records across four sources on file — the county's jail roster, the state prison system and two federal lists — with the statewide court source for Texas not on file yet. That is a materially different sentence from "no criminal records found", and it is the one to put in front of a reviewer. (The codes and counts above are illustrative; read yours from the response.)
Reading the result
| Field | Values | Read as |
|---|---|---|
sources[].status | live · pending | On file today, or known and not yet available. Only live rows ever contribute records. |
sources[].health.lastSuccessAt / ageSeconds | ISO timestamp / integer, or null | When the stored copy was last refreshed. null = never. Computed when the catalog is served (cached up to five minutes). |
sources[].health.freshFor | [] · ["weekly"] · ["daily", "weekly"] | The tiers the snapshot falls inside; a restatement of ageSeconds, not a filter. |
sources[].recordType | County jail roster · Court records · Prison system · Statewide registry · Warrant system · Exclusion list · Records | What kind of records the source holds, in words. The record-level twin is recordCategory. |
catalog | {total, live, comingSoon, states} | Totals over the same rows as sources[], so the two always agree. |
coverage.scope | {state, county, countyRecognized} | state is null unless a two-letter state was sent. countyRecognized: false = no source of that county's own is in the catalog. |
coverage.sourcesOnFile / sourcesPending | integers (also on counts) | Sources behind this answer, and sources in scope not on file yet. sourcesPending > 0 → read pendingSources before calling an empty result a clear. |
coverage.pendingSources[] | present when state was sent | The pending sources, named by code. |
coverage.sources[] | present with include: ["coverage"] and a state | Every source in scope with its status. |
sourceStatus[] | [] on a cached search | Per-source outcomes exist only for live checks; coverage is the cached-search equivalent. |
Edge cases and pitfalls
- "Not on file yet" is not "searched and found nothing". A
pendingsource in scope means nothing from it could have been returned. CarrysourcesPendingand the names into your outcome text. - `coverage` is absent, not empty, when the catalog cannot be read. Treat a missing key as "unknown coverage" and surface it; do not default it to zero pending.
- No `state` means an unscoped `coverage.scope.state: null` and no
pendingSources. If you want the coverage sentence, send the state. - Freshness is per source, not per record. A record's own currency is
sources[].lastCheckedAtand itslifecycle; the catalog'shealthdescribes the source's last refresh. - The catalog grows. Counts change as coverage grows; never hard-code a total or a code list. The public endpoint is cheap and cacheable (
Cache-Control: public, max-age=300). - `503 catalog_unavailable` means the catalog file could not be read; the gateway serves its last cached copy when it can. Retry with backoff; do not fail the search path on it.
- `GET /v1/criminal/jurisdictions` is the keyed twin with
recordKindandliveflags but nohealth; use it for codes,sourcesfor freshness.
Production checklist
- Cache
GET /v1/criminal/sourcesfor five minutes and alert when a source you depend on leavesfreshForor turnspending. - Send
stateon every search; storecoverage.sourcesOnFile,sourcesPendingandpendingSources[].codewith the result. - Word every clear outcome as "no records across N sources on file" and name the pending ones.
- Use a live check for a point-in-time answer from a specific source; never infer it from
health. - Keep the Not-a-consumer-report notice (
legal.notice) with any coverage statement you show.