# Build an inmate search tool: an inmate search API tutorial

> One endpoint, two record kinds and a handful of custody fields are the whole tool. This page walks the request, the fields to show, sorting and paging, coverage by jurisdiction, live re-verification and the UI rules that keep a booking from reading as a conviction.

- **HTML:** https://offendersearch.app/docs/learn/build-an-inmate-search-tool
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Docs index for agents:** https://offendersearch.app/llms.txt

## What an inmate search tool returns: doc_inmate and jail_booking records

An inmate search tool answers one question: is this person in custody, where, and since when. The [inmate search API](/inmate-search-api) is `POST /v1/criminal/search` on the [criminal records API](/criminal-records-api); the same endpoint returns court cases and warrants, and an inmate tool reads two of its record kinds. `recordType` is an open string from the source; `recordCategory` is the closed token to branch on.

| recordType | recordCategory | What it is | Custody fields that carry data |
| --- | --- | --- | --- |
| `doc_inmate` | `prison` | A state or federal department of corrections record: a sentenced person in a prison system. | `currentStatus`, `currentlyIncarcerated`, `custodyFacility`, `dateIncarcerated`, `releaseDate`, `releaseDates`, `custody`, `parole`, `sentence` |
| `jail_booking` | `jail` | A county jail roster entry: an arrest-stage record with a charge at booking and, usually, no disposition yet. | `currentStatus`, `currentlyIncarcerated`, `custodyFacility`, `dateIncarcerated`, `actualReleaseDate`, `booking` |

Every key on a criminal record is always present, with `""`, `null`, `[]` or `{}` when the source publishes nothing, so a tool reads the same shape for every record and never guards for a missing key. The [criminal record object](/docs/criminal/record-object) lists all of them; the [record types guide](/docs/guides/criminal/record-types) explains how a court case, a booking, a prison record and a warrant differ.

Everything below runs on the [sandbox](/sandbox) at `https://sandbox.offendersearch.app` with an `os_test_` key from the dashboard. A test key is free, nothing is billed, and the limits are 60 calls a minute and 2,000 a day per key. Production is `https://api.offendersearch.app` with an `os_live_` key and the same shapes; pricing is $0.15 a call, $0.11 past 2,000 calls in a month, 25 free calls, no card. Python and Node.js SDKs ship with the first registry release; see [SDKs](/sdk). Until then the raw HTTP calls below are the integration.

## The inmate search request: a last name and a state

The query needs `lastName`; without it the search is `422` with code `guard_unbounded_query`. A `state` scopes the search to that state's sources and is what makes `include: ["coverage"]` answer with the sources in scope. `firstName` narrows the candidates; `dob` as `YYYY-MM-DD` is the only route to a confirmed identity (`matchState: "dob_match"`); `birthYear`, `birthMonth` and `age` are weaker alternatives; `county`, `city`, `zipcode`, `address` and `jurisdiction` narrow the place. `page` and `perPage` (1–200) sit at the top level, not inside `query`.

**curl**
```bash
# An inmate search: a last name and a state. The sandbox answers fixed scenarios; nothing is billed.
export OFFENDERSEARCH_API_KEY=os_test_...   # minted in the dashboard

curl -X POST https://sandbox.offendersearch.app/v1/criminal/search \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": {"firstName": "In", "lastName": "Custody", "dob": "1979-12-31", "state": "AZ"},
       "filters": {"recordCategory": ["prison", "jail"]},
       "sort": "relevance",
       "include": ["coverage"],
       "page": 1, "perPage": 50}'
```

**Python**
```python
import os, requests

HOST = "https://sandbox.offendersearch.app"          # api.offendersearch.app + an os_live_ key in production
HEADERS = {"X-API-Key": os.environ["OFFENDERSEARCH_API_KEY"], "Content-Type": "application/json"}

def inmate_search(last: str, state: str, first: str | None = None, dob: str | None = None, page: int = 1) -> dict:
    query = {"lastName": last, "state": state}            # lastName is required; state scopes coverage
    if first: query["firstName"] = first
    if dob:   query["dob"] = dob                           # YYYY-MM-DD: the only route to dob_match
    body = {
        "query": query,
        "filters": {"recordCategory": ["prison", "jail"]}, # custody records only
        "sort": "relevance",
        "include": ["coverage"],                           # which sources stood behind the answer for this state
        "page": page, "perPage": 50,                       # perPage 1–200, top level
    }
    r = requests.post(f"{HOST}/v1/criminal/search", headers=HEADERS, json=body, timeout=130)
    r.raise_for_status()
    return r.json()

res = inmate_search("Custody", "AZ", first="In", dob="1979-12-31")
print(res["status"], res["counts"]["records"], "records;", res["counts"]["sourcesPending"], "sources pending")
for rec in res["records"]:
    print(rec["recordType"], rec["name"]["last"], rec["currentStatus"], rec["custodyFacility"],
          rec["releaseDate"], rec["matchState"])
for src in res.get("coverage", {}).get("sources", []):
    print("coverage:", src["code"], src["status"])
```

**Node**
```javascript
// Node 18+: fetch is built in
const HOST = "https://sandbox.offendersearch.app";   // api.offendersearch.app + an os_live_ key in production
const headers = { "X-API-Key": process.env.OFFENDERSEARCH_API_KEY, "Content-Type": "application/json" };

export async function inmateSearch({ last, state, first, dob, page = 1 }) {
  const query = { lastName: last, state };               // lastName is required; state scopes coverage
  if (first) query.firstName = first;
  if (dob) query.dob = dob;                              // YYYY-MM-DD: the only route to dob_match
  const res = await fetch(HOST + "/v1/criminal/search", {
    method: "POST", headers,
    body: JSON.stringify({
      query,
      filters: { recordCategory: ["prison", "jail"] },  // custody records only
      sort: "relevance",
      include: ["coverage"],                             // which sources stood behind the answer for this state
      page, perPage: 50,                                 // perPage 1–200, top level
    }),
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  return res.json();
}

const res = await inmateSearch({ last: "Custody", state: "AZ", first: "In", dob: "1979-12-31" });
console.log(res.status, res.counts.records, "records;", res.counts.sourcesPending, "sources pending");
for (const rec of res.records) {
  console.log(rec.recordType, rec.name.last, rec.currentStatus, rec.custodyFacility, rec.releaseDate, rec.matchState);
}
for (const src of res.coverage?.sources ?? []) console.log("coverage:", src.code, src.status);
```

The sandbox name "In Custody" with `dob` `1979-12-31` returns one `doc_inmate` record in AZ with `currentlyIncarcerated: true`, a `custodyFacility` and a release date. "Arrest Only" with `dob` `1992-04-20` returns one `jail_booking` in FL with a charge at booking and no disposition. "Clear Record" and any name that is not a scenario, such as Jordan Rivera, 1988-04-12, return a complete answer with zero records. "Possible Match" returns three records in three states with no date of birth on any, which is the name-only case the UI section covers.

The envelope is `status, counts { records, recordsReturned, sourcesQueried, sourcesComplete, sourcesIncomplete, candidatesCapped, sourcesOnFile, sourcesPending }, page, perPage, totalPages, sourceStatus[], legal { notice }, searchId, records[]`. A cached search has `sourcesQueried: 0`, because nothing was asked live.

## Custody fields an inmate search result carries

The custody picture lives in a few record-level fields plus four objects:

| Field | Type | Meaning |
| --- | --- | --- |
| `currentStatus` | string | The source's status, passed through: usually `in_custody`, `released`, `on_parole`, `on_probation`, `active_warrant`, …; `unknown` means the source published none. Branch on `normalized.custodyStatus` for a closed set. |
| `currentlyIncarcerated` | boolean | In custody as of the last check. `false` is not "released": it is also the value for someone never booked. Read `everIncarcerated` beside it. |
| `custodyFacility` | string \| null | The facility, where published. |
| `dateIncarcerated` | string \| null | Booking or admission date. |
| `releaseDate` | string \| null | The projected or scheduled release. |
| `actualReleaseDate` | string \| null | A release that has happened. Distinct from `releaseDate`. |
| `releaseDates` | object | `projected`, `earliest`, `maximum`, `supervisionEnds`, `parole`: the dates a prison publishes, ISO or `null`. |
| `custody` | object \| null | `custodyClass`, `description`, `facilityType`. |
| `parole` | object \| null | `eligibilityDate`, `nextReviewDate`, `reviewStatus`, `lastDecision`. |
| `booking` | object | `bookingNumber`, `bookingDate`, `arrestDate`, `arrestingAgency`, `heldForAgency`, `arrestingOfficer`, `housing`, `daysInJail`, `priorBookings`, `arrestTime`, `bookingTime`, `bookingType`, `releaseReason`, `bond`, `holds[]`. Carries data on jail records; `null` keys elsewhere. |

The "In Custody" record, trimmed to the custody keys:

*doc_inmate: "In Custody", dob 1979-12-31*
```json
{
  "recordType": "doc_inmate",
  "recordCategory": "prison",
  "externalId": "SBX-DOC-310045",
  "personRef": "prs_…",
  "name": { "first": "In", "middle": "Sample", "last": "Custody", "suffix": "" },
  "aliases": ["I. S. Custody"],
  "dob": { "date": "1979-12-31", "birthYear": 1979, "birthMonth": 12, "age": null, "ageAsOf": null, "dobPrecision": "full" },
  "state": "AZ",
  "county": "",
  "currentStatus": "in_custody",
  "currentlyIncarcerated": true,
  "everIncarcerated": true,
  "dateIncarcerated": "2019-04-02",
  "releaseDate": "2031-04-01",
  "actualReleaseDate": null,
  "custodyFacility": "Sample State Correctional Facility",
  "custody": { "custodyClass": "Medium", "description": "General population", "facilityType": "State prison" },
  "parole": { "eligibilityDate": "2029-10-01", "nextReviewDate": null, "reviewStatus": "Not yet eligible", "lastDecision": "" },
  "sentence": { "length": "12 years", "minDate": "2019-04-02", "maxDate": "2031-04-01", "years": 12 /* … */ },
  "priorFelony": true,
  "matchConfidence": 0.99,
  "matchState": "dob_match",
  "unverified": false,
  "matchBasis": "lastName:exact firstName:exact dob:match",
  "cases": [
    {
      "caseNumber": "SBX-CR-2018-007788",
      "court": "Sample County Superior Court",
      "state": "AZ", "level": "state", "filedDate": "2018-06-01", "status": "Sentenced",
      "charges": [
        { "statute": "SAMPLE 13-1904", "description": "Armed robbery", "severity": "felony",
          "disposition": "Guilty", "dispositionDate": "2019-03-15", "offenseDate": "2018-05-20", "plea": "Guilty", "counts": 1 }
      ]
      /* … events, nextHearing, sentence */
    }
  ],
  "sources": [
    { "code": "AZ-SAMPLE", "level": "state", "recordUrl": "https://offendersearch.app/docs/sandbox",
      "lastCheckedAt": "2026-10-01T00:00:00+00:00", "live": false }
  ]
  /* … every other key is present ("" / null / [] / {} when the source publishes nothing) */
}
```

And the "Arrest Only" booking. Note `currentStatus: "released"`, `actualReleaseDate` set, `releaseDate` null, a case at `status: "Booked"` with `court: ""`, and a charge whose `disposition` is `""`:

*jail_booking: "Arrest Only", dob 1992-04-20*
```json
{
  "recordType": "jail_booking",
  "recordCategory": "jail",
  "externalId": "SBX-BK-2026-004512",
  "personRef": "prs_…",
  "name": { "first": "Arrest", "middle": "", "last": "Only", "suffix": "" },
  "state": "FL",
  "county": "Sample",
  "currentStatus": "released",
  "currentlyIncarcerated": false,
  "everIncarcerated": true,
  "dateIncarcerated": "2026-08-03",
  "releaseDate": null,
  "actualReleaseDate": "2026-08-04",
  "custodyFacility": "Sample County Jail",
  "matchState": "dob_match",
  "cases": [
    {
      "caseNumber": "SBX-BK-2026-004512", "court": "", "state": "FL", "level": "county",
      "filedDate": "2026-08-03", "status": "Booked",
      "charges": [
        { "statute": "SAMPLE 316.193", "description": "Driving under the influence", "severity": "misdemeanor",
          "disposition": "", "offenseDate": "2026-08-03", "plea": "", "counts": 1 }
      ]
    }
  ],
  "booking": {
    "bookingNumber": "SBX-B-2026-0101", "bookingDate": "2026-09-30", "arrestDate": "2026-09-30",
    "arrestingAgency": "Sample County Sheriff's Office", "heldForAgency": "Sampleville Police Department",
    "housing": "Pod B-12", "holds": ["Sample County probation hold"]
    /* … arrestingOfficer, daysInJail, priorBookings, arrestTime, bookingTime, bookingType, releaseReason, bond */
  },
  "sources": [
    { "code": "FL-SAMPLE", "level": "county", "recordUrl": "https://offendersearch.app/docs/sandbox",
      "lastCheckedAt": "2026-10-01T00:00:00+00:00", "live": false }
  ]
}
```

Two more fields matter to an inmate tool: `identifiers.inmateNumber` (the DOC or inmate number) with `identifiers.stateId`, and `mugshotUrl` where the source publishes a photo.

## Sort, filter and page inmate search results

An inmate tool usually wants custody records only, newest first, a page at a time. Four request keys do that.

- **`filters`.** Filters are ANDed; values inside one list are ORed. `recordCategory: ["prison", "jail"]` keeps `doc_inmate` and `jail_booking` records; `custodyStatus: ["in_custody"]` keeps people in custody now; `lookbackYears`, `offenseDate`, `filedDate`, `severity`, `chargeText`, `sources` and `hasPhoto` are among the rest. See [filtering](/docs/criminal/filtering).
- **`sort`.** `relevance` (default), `offenseDate`, `filedDate` or `dispositionDate`; a date sort puts the record's latest date of that kind first and records without one last. It applies when `filters` or `locations` are sent.
- **`page` and `perPage`.** `perPage` is 1–200 at the top level; `totalPages` tells you when to stop; an out-of-range page is `422` with code `invalid_page`.
- **`include`.** `["coverage"]` adds `coverage.sources[]` when the query names a `state`; `["facets"]` adds counts over every matched record, including `facets.custodyStatus`, to a search that sends `filters`; `["sourceData"]` empties `omittedFields`.

A person can appear in both a jail roster and a prison system; the two records have different `externalId` values and may share a `personRef`. Group by `personRef` for one card per person and keep the records separate underneath, because each has its own `sources[].recordUrl`.

## Inmate search coverage by jurisdiction: GET /v1/criminal/sources

A tool should be able to say which jails and prison systems stood behind an answer, and which it could not reach. `GET /v1/criminal/sources` needs no key and lists every source the product knows about with its `code` (`TX-BEXAR`, `TX-DOC`), `name` and `status` (`live` or `pending`), with `catalog` totals over the same rows. The code is the same string a record's `sources[].code` carries and a `live` block's `jurisdictions[]` takes.

*The catalog, and the per-search view*
```bash
# Coverage by jurisdiction. No key needed.
curl https://api.offendersearch.app/v1/criminal/sources

# { "catalog": { "total": …, "live": …, "comingSoon": … }, "sources": [ { "code": "TX-BEXAR", "name": "…", "status": "live" | "pending", … } ] }
# A code here is the same code a record's sources[].code carries and a live block's jurisdictions[] takes.

# The same answer scoped to one search: include ["coverage"] with a state in the query
# "coverage": { "scope": { "state": "AZ", "county": null }, "sourcesOnFile": 5, "sourcesPending": 1,
#               "pendingSources": [ … ], "sources": [ { "code": "AZ-SAMPLE", "status": "…" }, … ] }
```

Per search, `include: ["coverage"]` with a `state` in the query adds a `coverage` block: `scope { state, county, countyRecognized }`, `sourcesOnFile`, `sourcesPending`, `pendingSources[]` and `sources[]` with each source's `status`. `countyRecognized: false` on a county search means only statewide and federal sources stood behind the answer. Show that to the user: "searched 5 sources for AZ; 1 pending" is an honest line. The [coverage reference](/docs/criminal/coverage) explains the statuses.

## Live re-verification in an inmate search tool

A cached answer is the continuously updated dataset as of `sources[].lastCheckedAt`. Custody changes by the hour, so a tool that shows "in custody" should offer to re-verify with the jurisdiction. Add a `live` block to the same request: `jurisdictions: ["AZ-SAMPLE"]` names the sources; `scope: "matched"` re-verifies the sources of the records already matched. With two or fewer sync-eligible sources the answer is `200` inline; more than two is `202` with `searchId` (`crs_…`), `status: "pending"`, `mode: "async"`, `liveSourcesRequested`, a `poll` path and a `reason`, and `GET /v1/criminal/searches/{searchId}` returns the envelope when it is done. The price is $0.02 per completed source.

*Live re-verification*
```bash
# Re-verify a record with the jurisdiction. Up to two sync-eligible sources answer inline (200);
# more than two answer 202 with a poll URL. $0.02 per completed source.
curl -X POST https://sandbox.offendersearch.app/v1/criminal/search \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": {"firstName": "In", "lastName": "Custody", "dob": "1979-12-31", "state": "AZ"},
       "live": {"jurisdictions": ["AZ-SAMPLE"]}}'
# 200 → the envelope; counts.sourcesQueried / sourcesComplete / sourcesIncomplete count the live sources,
#        sourceStatus[] lists each, and a re-verified record carries sources[].live: true

# More than two sources, or "scope": "matched" across a wide result:
#   202 { "searchId": "crs_…", "status": "pending", "mode": "async", "liveSourcesRequested": 4,
#         "poll": "/v1/criminal/searches/crs_…", "reason": "…" }
curl https://sandbox.offendersearch.app/v1/criminal/searches/crs_… -H "X-API-Key: $OFFENDERSEARCH_API_KEY"
```

A re-verified record carries `sources[].live: true` and `liveChecked: true`; `sourceStatus[]` lists each live source. On a live search with a full `dob`, `dobVerification` can read `source_confirmed` or `source_not_confirmed`. Persist the `searchId` from a `202` before polling; there is no idempotency header, so a resubmitted search is a second billed one. The [cached vs live guide](/docs/guides/criminal/cached-vs-live) has the timing and retry rules.

## Build the inmate search UI: a booking is not a conviction

Four rules keep an inmate search tool honest.

1. **Label the kind.** A `doc_inmate` record is a prison record; a `jail_booking` is a jail booking. Use `recordCategory` for the label and never collapse the two into "criminal record".
2. **A booking is not a conviction.** A jail booking is arrest-stage: its charge is the charge at booking, `cases[].charges[].disposition` is `""` until a court decides, and `cases[].status` reads `Booked`. Read `disposition` to decide whether a record is a conviction, and say "booked on" rather than "convicted of" on a booking card. The [arrests, charges and convictions guide](/docs/guides/criminal/arrests-charges-convictions) is the reference.
3. **Show `sources[].recordUrl`.** Every record links to the jail's or prison system's own page. Put the link on the card with `sources[].lastCheckedAt`, so the user can see the current roster themselves.
4. **Never show a name-only match as a confirmed identity.** `matchState: "dob_match"` is the only confirmation. `no_dob_age_year` (`unverified: true`), `year_match` and `age_match` are possible matches; `matchConfidence` is a ceiling on the evidence and a `0.95` on a name-only record is still a name match. Render those as "possible match" with the published `dob`, `physical` and `mugshotUrl` beside them, and keep them out of any view titled "confirmed".

Every response carries `legal.notice`: "Not a consumer report…". An inmate search tool is a lookup, not an eligibility decision; a tool whose answer decides hiring, tenancy, credit or insurance is an FCRA order through [FCRA Partner Access](/fcra), and that is a question for counsel.

To hear about a new booking or a custody change without re-searching, create a person monitor with `product: "criminal"`, `type: "person"`, the person and a `webhookUrl`. It answers `201` with `channels.webhookSecret` shown once and sends a signed `monitor.alert` on `new`, `changed` and `removed` events; $3 a month per person monitor, $10 per location monitor. Verify `X-Offendersearch-Signature` on the raw body before parsing.

*Monitor for a new booking*
```bash
# Watch one person for a new booking or custody change. $3 a month per person monitor.
curl -X POST https://sandbox.offendersearch.app/v1/monitors \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"product": "criminal", "type": "person",
       "label": "In Custody — custody watch",
       "person": {"firstName": "In", "lastName": "Custody", "dob": "1979-12-31"},
       "minConfidence": "dob_match",
       "channels": {"webhookUrl": "https://hooks.example.com/hooks/offendersearch"}}'
# 201 … "channels": { "webhookSecret": "whsec_…" }   ← shown once
# Alerts: { "type": "monitor.alert", "event": "new" | "changed" | "removed", "alertId": "hit_…",
#           "detail": { "name", "jurisdiction", "locality", "matchLabel", "matchConfidence", "personRef", "recordId" } }
```

## Build an inmate search tool in seven steps

1. **Mint a test key.** Create an os_test_ key in the dashboard and call https://sandbox.offendersearch.app. A test key is free and nothing is billed.
2. **Send a last name and a state.** POST /v1/criminal/search with query.lastName (required), query.state, and firstName and dob when you have them; page and perPage (1–200) go at the top level.
3. **Keep custody records with filters.** Send filters.recordCategory ["prison", "jail"], a sort, and include ["coverage"] to see which sources stood behind the answer.
4. **Display the custody fields.** Show currentStatus, currentlyIncarcerated, custodyFacility, dateIncarcerated, releaseDate and actualReleaseDate, plus custody, parole, releaseDates and booking where published.
5. **Label the kind and link the source.** Label each record as a prison record or a jail booking, show sources[].recordUrl, and treat a charge with an empty disposition as arrest-stage, not a conviction.
6. **Never show a name-only match as an identity.** Only matchState dob_match confirms identity; render no_dob_age_year, year_match and age_match records as possible matches.
7. **Re-verify live and monitor.** Add a live block to re-verify with the jurisdiction (two sources inline, more via 202 and poll) and create a criminal person monitor with a webhookUrl.

The [inmate search](/inmate-search) page is the product view for the people who use your tool.

## Inmate search API questions

### Can I search by inmate number instead of a name?

The search query is name-based and `lastName` is required. A record carries `identifiers.inmateNumber`, so once you have found a person you can match on the number in your own store, and `GET /v1/criminal/records/{recordId}` re-fetches a record by its id.

### Does currentlyIncarcerated: false mean the person was released?

No. It is also the value for someone never booked. Read `everIncarcerated` and `currentStatus` beside it, and `actualReleaseDate` for a release that has happened.

### Is a jail booking a criminal record?

It is an arrest-stage record. The charge is the charge at booking and `disposition` is empty until a court decides. Label it as a booking and never as a conviction.

### How do I know which jails and prisons were searched?

`GET /v1/criminal/sources` lists the catalog with each source's `status`; `include: ["coverage"]` on a search with a `state` adds `coverage.sources[]` for that search, with `sourcesPending` and `pendingSources[]` for what is not available yet.

### How fresh is a cached answer, and how do I get a current one?

Each record's `sources[].lastCheckedAt` says when the source was last checked; the dataset is continuously updated. For a current answer, send a `live` block: two sources inline, more via `202` and `GET /v1/criminal/searches/{searchId}`, at $0.02 per completed source.

## What this page does not cover

- Court cases, dispositions and sentences in depth: the `cases[]` side of the same record.
- Warrants and wanted records (`recordType: "warrant"`, `"wanted_fugitive"`).
- Bulk lookups across many names with `POST /v1/criminal/batch` (cached rows only).
- Healthcare exclusion records (`healthcare_exclusion`), returned beside criminal records.

## Related

- [Court records API tutorial](https://offendersearch.app/docs/learn/court-records-api-tutorial)
- [Search criminal records by name and date of birth](https://offendersearch.app/docs/learn/search-criminal-records-by-name-and-dob)
- [Monitor a person with webhooks](https://offendersearch.app/docs/learn/monitor-a-person-with-webhooks)
- [Webhook example: receive, verify, retry](https://offendersearch.app/docs/learn/webhook-example)
- [Dispositions guide](https://offendersearch.app/docs/guides/criminal/dispositions)
- [Matching and confidence (criminal)](https://offendersearch.app/docs/guides/criminal/matching-and-confidence)
- [Normalized fields](https://offendersearch.app/docs/criminal/normalized-fields)
- [Criminal pagination](https://offendersearch.app/docs/criminal/pagination)
- [Criminal async searches](https://offendersearch.app/docs/criminal/async)
- [Criminal batch](https://offendersearch.app/docs/criminal/batch)
- [Warrant search](https://offendersearch.app/warrant-search)
- [Learn APIs by example](https://offendersearch.app/docs/learn)
