Criminal Records API · v1.0.0

Jurisdiction and location filtering

Two kinds of place: where the record is (jurisdiction) and where the person lives (residence). The request fields tell them apart, and so should you.

What you'll build

A search that is scoped to the places that matter for your case: the state and county a facility sits in, the places a person has lived, or the specific jurisdiction a records website belongs to. Scoping is how a common-name search stays answerable (state and firstName are what the query_too_broad refusal asks for) and how a reviewer sees the records that could be the person rather than every namesake in the country.

Before you start

  • A test key (os_test_…) on https://sandbox.offendersearch.app. The sandbox people live in Sampleville, ZIP 00000; Felony Record is an Ohio (OH) court record, Misdemeanor Record a Texas (TX) one, so state scoping is visible there.
  • Jurisdiction codes. <ST>-<COUNTY> for a county (TX-BEXAR, TX-PALM-BEACH-style hyphenation) and <ST>-<RECORD-KIND> for a statewide or federal system (TX-DOC, OK-COURT, US-WARRANT). Take them from GET /v1/criminal/jurisdictions or GET /v1/criminal/sources; some county codes carry more tokens than the county name, so never build them by hand.
  • The full filter reference: Filtering & locations and Jurisdictions & codes.
Flat place fields and locations[] are exclusive. query.state, county/jurisdiction, city, zipcode/zip and address/streetAddress are the simple form. locations[] is the rich form (kind, radius, up to ten places, ORed). Send one form or the other; both together is 422 ambiguous_location.

Step by step

  1. Scope by jurisdiction. state (and optionally county) filter on the jurisdiction that holds the record — the court, jail or agency — not on where the person lives. state also drives the coverage block.
  2. Scope by residence. city, zipcode and address match the record's published residence address. A record with no residence on file can never match them, so they are narrower than they look.
  3. Use locations[] for several places or a radius. Each entry has a kind (residence, jurisdiction, any), a place, and optionally lat/lng/radiusMiles (0.1–100). A record is kept when it matches any entry and lists which in matchedLocations.
  4. Look up and resolve codes. GET /v1/criminal/jurisdictions?state=TX&live=true lists what is covered and live-reachable; POST /v1/criminal/resolve maps a records website you already know to its code. Neither is metered.
# 1. jurisdiction scope — OH returns the record, TX does not
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": "Felony", "dob": "1975-06-15", "state": "OH" } }'

# 2. several places, ORed, with the rich form (no flat state/county in query)
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": "Rivera", "firstName": "Jordan", "dob": "1988-04-12" },
        "locations": [ { "kind": "residence", "zipcode": "77060" },
                       { "kind": "jurisdiction", "state": "OK" },
                       { "kind": "any", "lat": 32.78, "lng": -96.8, "radiusMiles": 25 } ] }'

# 3. what is covered in Texas and reachable live
curl -s "https://sandbox.offendersearch.app/v1/criminal/jurisdictions?state=TX&live=true" \
  -H "X-API-Key: os_test_your_key"

# 4. map a website you know to a code
curl -s https://sandbox.offendersearch.app/v1/criminal/resolve \
  -H "X-API-Key: os_test_your_key" -H "Content-Type: application/json" \
  -d '{ "website": "https://sheriff.examplecounty.gov/inmates" }'
A locations[] answer, trimmed to the scoping keys
{
  "status": "complete",
  "counts": { "records": 2, "recordsReturned": 2, "sourcesQueried": 0, "sourcesComplete": 0, "sourcesIncomplete": 0,
              "candidatesCapped": false, "exclusionsIncluded": false,
              "filteredOut": { "locations": 5, "noResidence": 3 }, "unknownKept": {}, "quarantined": 0 },
  "appliedFilters": {
    "locations": [ { "kind": "residence", "zipcode": "77060" },
                   { "kind": "jurisdiction", "state": "OK" },
                   { "kind": "any", "lat": 32.78, "lng": -96.8, "radiusMiles": 25 } ],
    "unknown": "keep", "chargeMatch": "any"
  },
  "records": [
    { "state": "TX", "county": "Harris", "address": { "type": "residence", "line1": "…", "city": "Houston", "county": "Harris", "state": "TX", "zipcode": "77060", "lat": null, "lng": null },
      "matchedLocations": [0] },
    { "state": "OK", "county": "Tulsa", "address": null, "matchedLocations": [1] }
  ]
}
GET /v1/criminal/jurisdictions?state=TX&live=true and POST /v1/criminal/resolve, trimmed
{
  "coverage": { "jurisdictions_total": 1410, "jurisdictions_live_connector": 727, "jurisdictions_pending_connector": 683, "states_covered": 50 },
  "jurisdictions": [
    { "code": "TX-BEXAR", "state": "TX", "county": "Bexar", "recordKind": "jail-roster", "level": "county", "live": true },
    { "code": "TX-DOC",   "state": "TX", "county": "(statewide)", "recordKind": "doc-inmate", "level": "state", "live": true }
  ],
  "count": 2
}

{ "input": "https://sheriff.examplecounty.gov/inmates", "supported": true, "code": "TX-BEXAR",
  "state": "TX", "county": "Bexar", "note": "equivalent data available for this jurisdiction" }

Reading the result

FieldWhereRead as
query.state, query.countyrequestJurisdiction filters: the state and county that hold the record. county accepts Harris or Harris County; jurisdiction is an alias.
query.city, query.zipcode, query.addressrequestResidence filters on the published address. zipcode is a prefix match ("750" reaches 75001); address is a case-insensitive substring of line1; city is exact.
locations[].kindrequestjurisdiction tests state/county of the record; residence tests the address; any tests either — but a radius, city or zipcode always describe a residence and are 422 invalid_location on jurisdiction.
locations[].lat/lng/radiusMilesrequestAll three together; 0.1–100 miles. A residence without coordinates cannot be tested and is counted under filteredOut.noResidence.
records[].matchedLocationsresponseIndexes into your locations[] that this record satisfied. Present only when locations was sent.
counts.filteredOutresponseWhat the scoping removed, by reason (locations, noResidence, plus any filters keys). Present whenever filters, locations, a non-default sort or include: ["facets"] was sent.
appliedFiltersresponseThe locations as validated — the keys each was read as. Use it to debug a filter that matched nothing.
coverage.scoperesponse{state, county, countyRecognized}: countyRecognized: false means no source of that county's own is in the catalog and only statewide/federal sources answered.
jurisdictions[].recordKindGET /v1/criminal/jurisdictionsjail-roster, court, doc-inmate, registry, warrant, exclusion. Maps to a record's recordCategory (jail, court, prison, registry, warrant, exclusion).
supported, code, notePOST /v1/criminal/resolvesupported: false with code: null is an outcome, not an error. The response names only state, county and the neutral code.

Edge cases and pitfalls

  • `state` filters the jurisdiction, not the residence. A person who lives in Oklahoma with a Texas court case is found by state: "TX", not "OK". Use locations[] with kind: "any" when you mean "either".
  • A residence filter silently excludes records with no address. Many court and prison records publish none. Prefer state/county for scoping and use city/zipcode only when the address matters.
  • A county, city or ZIP alone does not rescue a too-broad name. The candidate walk is bounded by surname, first name and state; a query_too_broad refusal asks for firstName or state.
  • `locations` with a live search of more than two sources is `422 filters_with_async_live`. Scoping works with stored searches and with live checks that run inline; narrow live.jurisdictions first.
  • Limits are refusals, not clamps. An empty locations list is invalid_location; eleven entries is too_many_locations; radiusMiles: 150 is radius_too_large; radiusMiles: 0.05 is invalid_location.
  • `query.website` scopes; `live.websites` verifies. A website in query resolves to a state/county scope for a cached search (an explicit state/county wins over it); an unsupported site adds a WEBSITE NOT COVERED: sentence to warnings[] and no scope. The same site in live.websites becomes a live check.
  • `sort` is validated on every search but applied only with filters or locations. sort: "newest" is 422 invalid_filter_value; the values are relevance, offenseDate, filedDate, dispositionDate.

Production checklist

  • Load jurisdiction codes from GET /v1/criminal/jurisdictions at startup or daily; never hard-code them.
  • Choose one form per request: flat query place fields or locations[].
  • Send state on every search where you know it; it narrows, drives coverage, and keeps common names answerable.
  • Log appliedFilters and counts.filteredOut with each search so a reviewer can see what was excluded and why.
  • Handle invalid_location, too_many_locations, radius_too_large and ambiguous_location as request bugs, not retries.

Product overview