Arrest records API example: look up a jail booking by name
One request to the criminal records API, one jail_booking record back: what an arrest record carries, how to tell a current booking from an old one, how to follow it to court, and how to be told when a new booking appears.
Updated November 3, 2026
An arrest record is not a conviction: what the arrest records API returns
A jail booking is an arrest-stage record. It says that a person was booked into a county jail on a date, by an agency, on one or more charges. It does not say that a court found them guilty, and most of the time no court has looked at it yet. The criminal records API returns a booking as a record whose recordType is jail_booking, with the charge at booking in cases[].charges[] and the disposition of that charge empty. An empty disposition is the whole point: nothing has been decided. A conviction is a different record kind, court_case, with a filled disposition. Say this in your product copy, in your reviewer UI and in your logs, because the single most common misuse of arrest data is reading a booking as a verdict.
This page walks through one booking end to end on the sandbox, where the "Arrest Only" person with date of birth 1992-04-20 returns exactly one jail_booking record. The same request on https://api.offendersearch.app with an os_live_ key searches the production dataset at $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. Until then the raw HTTP calls below are the integration.
| Record kind | `recordType` | What it tells you | Decides a conviction? |
|---|---|---|---|
| Jail booking | jail_booking | Arrested and booked; a charge at booking; booking.bookingDate, booking.arrestingAgency | No. disposition is "" |
| Court case | court_case | Charges with disposition and dispositionDate; a sentence when there is one | Yes, per charge |
| Prison or DOC record | doc_inmate | Custody status, custodyFacility, release dates | Implies a sentence; read the case for the charge |
| Warrant | warrant | currentStatus active_warrant; the warrant facts | No |
Look up arrest records with the API: the request
The endpoint is POST /v1/criminal/search. query.lastName is required; without it the API answers 422 with error.code guard_unbounded_query rather than return a lottery of namesakes. Add query.firstName, and query.dob as YYYY-MM-DD when you have it: a full date of birth is what turns a name match into an identification. In production you will almost always add query.state, and query.county when the roster you care about is one county. page and perPage (1 to 200) sit at the top level of the body, not inside query. The call is cached by default, which is what you want for an arrest lookup; the cached vs live guide covers when to add a live block.
# Arrest records by name, on the sandbox (free). lastName is required.
curl -X POST https://sandbox.offendersearch.app/v1/criminal/search \
-H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{"query": {"firstName": "Arrest", "lastName": "Only", "dob": "1992-04-20"}, "page": 1, "perPage": 50}'The response is the standard criminal envelope: status ("complete" or "partial"), counts (records, recordsReturned, sourcesQueried, sourcesComplete, sourcesIncomplete, sourcesOnFile, sourcesPending), warnings[], page, perPage, totalPages, sourceStatus[], legal.notice, searchId and records[]. Read status first: a "partial" answer with sourcesIncomplete above zero is a lower bound, not a clear, and the partial results guide says what to do with it. The 422 with a coded error object is the one error you will meet while developing; /docs/criminal/errors lists the vocabulary.
Reading a jail_booking arrest record: the keys that matter
Every criminal record carries the same keys, and every key is always present ("" / null / [] when the source publishes nothing), so a booking and a court case are read by the same code. The criminal record object reference lists them all; the subset an arrest lookup reads is below, in the shape the sandbox returns for "Arrest Only".
// The jail_booking record the sandbox returns for Arrest Only (captured 2026-11-03 from the
// sandbox code), trimmed to the keys this page reads. Every key is always present in the full
// response; "" / null / [] when the source publishes nothing.
{
"recordType": "jail_booking",
"name": {
"first": "Arrest",
"middle": "",
"last": "Only",
"suffix": ""
},
"dob": {
"date": "1992-04-20",
"birthYear": 1992,
"birthMonth": 4,
"age": null,
"ageAsOf": null,
"dobPrecision": "full"
},
"state": "FL",
"county": "Sample",
"currentStatus": "released",
"currentlyIncarcerated": false,
"matchState": "dob_match",| Key | Read it for |
|---|---|
recordType | jail_booking. Branch on it before anything else; a court case for the same person is a separate record. |
name, dob | Who the source says this is. dob.date is the full date when published; dob.dobPrecision says how precise it is. |
state, county | Where the booking happened. The county is the jail, not the person's address. |
currentStatus | Open text, the source's own status passed through: usually in_custody, booked or released on a booking, on_probation, active_warrant or excluded on other kinds, and unknown when the source published none. Branch on lifecycle, not on this string. |
booking | bookingNumber, bookingDate, arrestDate, arrestingAgency. The facts of the arrest itself. |
cases[].charges[] | The charge at booking: statute, description, severity, offenseDate, counts. disposition is "" and dispositionDate is null: nothing decided. |
sources[] | code (the jurisdiction, e.g. a county code), level "county" for a jail roster, recordUrl (the official page to cite), lastCheckedAt. |
lifecycle | firstSeen, lastSeen, presentInLastSweep, droppedOut: whether the booking is current. Next section. |
mugshotUrl | The booking photo when the source publishes one. Never show it as proof of identity (see below). |
physical, identifiers | Height, weight, hair and eye colour; stateId and fbiNumber when published. Useful to a reviewer comparing two records, never a match on their own. |
matchState, matchConfidence, unverified | Whether the record is the person you asked about. dob_match is the only automatic identification. |
Current or historical: reading lifecycle
A booking that is still on the jail roster and a booking from three years ago look alike in cases[]: both have a charge and no disposition. lifecycle is what separates them. presentInLastSweep true and droppedOut false means the person was on the roster the last time the source was checked, and lastSeen tells you when that was. droppedOut true means they were on it once (firstSeen) and are not any more; for a jail roster the most likely reason is release, and lastSeen is the last date they were listed. A dropped-out booking is still returned, flagged, because a person who was arrested in the past is still an arrest record. Show the dates, not just a label: "booked 2026-03-02, last on the roster 2026-03-09" is what a reviewer can act on.
From arrest to court case: following the charge to a disposition
A booking tells you a charge was filed at arrest. What happened next lives in a court_case record for the same person, when the county court publishes it and the case has been filed. Search the same query (same name, same dob, the booking's state and county) and read the records whose recordType is court_case. Match a case to the booking by cases[].caseNumber when the booking carries one, otherwise by the charge description and offenseDate, which should agree with the booking's arrestDate or sit a few days before it.
Then read each charge, not the case. cases[].charges[].disposition decides it: "Guilty" (or a jurisdiction's equivalent) with a dispositionDate is a conviction on that charge; "Dismissed", "Nolle prosequi", "Acquitted" and the like are not; "" means still pending. One case can carry both, as the sandbox's "Felony Record" person shows with a Guilty burglary and a Dismissed theft in the same cases[0]. The arrests, charges and convictions guide lists the disposition vocabulary you will meet across counties, and the court records API tutorial walks the court side of the same person. If no court_case comes back, say "no court record found in the sources on file", not "charges dropped": counts.sourcesPending tells you whether a court source for that county is not yet available.
Scope an arrest records API search by county, sort it and check coverage
Arrest records are county records. query.state narrows a search to one state; query.county narrows it to one county within that state. Send the county as a name ("Bexar"), and send state with it, because the same county name exists in several states and the API refuses an ambiguous place with 422 ambiguous_location. With a state in the query you can also ask for include: ["coverage"], which adds coverage.sources[]: every source in scope for that state or county, each with code, name, recordType, scope ("county", "state" or "federal") and status ("live" or "pending"). That is how you show a reviewer which county jail rosters the answer actually covers. sort: "offenseDate" puts the record with the newest offense date first when the request also sends filters or locations; a plain search keeps relevance order.
# Scope to one county and ask which county sources were in scope (production shape; synthetic subject)
curl -X POST https://sandbox.offendersearch.app/v1/criminal/search \
-H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" -H "Content-Type: application/json" \
-d '{"query": {"firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12", "state": "TX", "county": "Bexar"},
"include": ["coverage"], "sort": "offenseDate", "page": 1, "perPage": 50}'
# 200 — complete, zero records for this synthetic name; coverage.sources[] lists every source in scope
{ "status": "complete",
"counts": { "records": 0, "recordsReturned": 0, "sourcesOnFile": …, "sourcesPending": … },
"coverage": { "sources": [ { "code": "TX-BEXAR", "name": "Bexar County, TX — County Jail Roster",
"recordType": "County jail roster", "scope": "county", "status": "live" } ] },
"records": [] }
# The public catalog, no key: one row per jurisdiction with its scope, status and last-success time
curl https://api.offendersearch.app/v1/criminal/sources
# { "coverage": { "jurisdictions_total": … }, "sources": [ { "id": "TX-BEXAR", "scope": "county", "status": "live",
# "health": { "lastSuccessAt": "…", "ageSeconds": … } } ], "count": … }Before you build for a county, check it is covered: GET /v1/criminal/sources needs no key and lists one row per jurisdiction with id, name, scope, status and a health block with lastSuccessAt. The dataset is continuously updated; health.ageSeconds is the honest number for how current one jurisdiction is. The coverage reference covers the catalog and the place fields in full; the jail roster and arrest records pages describe what the roster sources carry.
Be told of a new arrest: a criminal person monitor
Re-running a search on a schedule to notice a new booking is the wrong shape: it is a billed call every time and most of them find nothing. A person monitor on the criminal dataset watches a named person continuously and sends a monitor.alert when a new record appears, changes, or is removed. Create it with POST /v1/monitors, product: "criminal", type: "person", the person (lastName required, firstName and dob recommended) and at least one channel: channels.webhookUrl, channels.email or channels.emails[]. Set minConfidence: "dob_match" when you send a date of birth and only want identified hits. The response is 201 with the monitor and, when you gave a webhook, channels.webhookSecret shown once. $3 a month per person monitor.
# A criminal person monitor: be told when a new booking, case or warrant appears for this person
curl -X POST https://sandbox.offendersearch.app/v1/monitors \
-H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" -H "Content-Type: application/json" \
-d '{"product": "criminal", "type": "person", "label": "Arrest Only — booking watch",
"person": {"firstName": "Arrest", "lastName": "Only", "dob": "1992-04-20"},
"minConfidence": "dob_match",
"channels": {"webhookUrl": "https://hooks.example.com/hooks/offendersearch"}}'
# 201 … "channels": {"webhookUrl": "…", "webhookSecret": "whsec_…"} ← shown once; store it
# What arrives at the webhook when something new is found (monitor.alert)
{
"type": "monitor.alert",
"event": "new",
"alertId": "hit_…",
"monitorId": "mon_…",
"monitorLabel": "Arrest Only — booking watch",
"product": "criminal",
"detail": { "name": "ARREST ONLY", "jurisdiction": "…", "locality": "…", "matchLabel": "dob_match",
"matchConfidence": 0.99, "personRef": "prs_…", "recordId": "…" },
"occurredAt": "…", "sentAt": "…"
}
# Header: X-Offendersearch-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>." + raw body)>The alert carries event ("new", "changed" or "removed"), alertId, monitorId, product and a detail block with name, jurisdiction, locality, matchLabel, matchConfidence, personRef and recordId. Treat it as a notification, not a record: fetch the record with GET /v1/criminal/records/{recordId} before you show anything, and verify the X-Offendersearch-Signature header on the raw body first. The receiver code is on the webhook example page; alert semantics are in the monitoring API reference. Location monitors are not yet available for the criminal dataset: product: "criminal" with type: "location" answers 200 { "status": "not_yet_available" } and creates nothing.
Mugshots, name-only matches and arrest record identity
Two things an arrest lookup must never do. The first is to show a mugshot as if it confirmed identity. mugshotUrl is the photo the jail published with the booking; it proves that the booked person looked like that, not that the booked person is your applicant. Show it to a reviewer beside the record, labelled as the booking photo, and keep the identity decision on matchState.
The second is to treat a name-only match as a hit. The sandbox's "Possible Match" person returns three records in three states with no date of birth on any of them, matchState no_dob_age_year and unverified true on each. That is a lead for a human, and the right UI says "3 possible records, none verified". dob_match with unverified false is the only automatic identification; year_match and age_match are weaker agreements you should surface as such; a record whose date of birth contradicts yours is not returned at all, which is why "Arrest Only" with a different date of birth is a clean zero. The false positives guide goes deeper, and the no-match and multiple-match results tutorial shows the UI states.
How to check if someone was arrested with the API, step by step
- Mint a test key and point at the sandbox. Create an
os_test_key in the dashboard, export it asOFFENDERSEARCH_TEST_KEYand callhttps://sandbox.offendersearch.app. Nothing on the sandbox is billed; 60 calls a minute and 2,000 a day per key. - Send lastName, firstName and dob.
POST /v1/criminal/searchwithquery.lastName(required),query.firstNameandquery.dobas YYYY-MM-DD. In production addquery.state, andquery.countywhen you know it. Sandbox: "Arrest Only" with 1992-04-20. - Keep only jail_booking records and check matchState. Filter
records[]onrecordType"jail_booking". Act ondob_matchwithunverifiedfalse; treatno_dob_age_yearas a lead for a reviewer, never as an identification. - Read the booking and the charge at booking. Read
booking.bookingDate,booking.arrestDateandbooking.arrestingAgency, thencases[].charges[]:description,severityandoffenseDate.dispositionis "" at this stage: an arrest, not a conviction. - Read lifecycle to tell current from historical.
presentInLastSweeptrue anddroppedOutfalse is a booking still on the roster;droppedOuttrue withlastSeenin the past is a historical one, most likely a release. - Follow the case to court. Search the same person again and read
court_caserecords:cases[].charges[].dispositionanddispositionDatedecide whether anything became a conviction. Opensources[].recordUrlto confirm. - Watch for a new booking.
POST /v1/monitorswithproduct"criminal",type"person", the person andchannels.webhookUrl. Amonitor.alertwithevent"new" arrives when a new record appears. $3 a month per person monitor.
Every response carries 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.)."* Keep it beside the result. If the lookup feeds a hiring, tenant, credit or insurance decision, it does not run here: it runs as an FCRA order on the FCRA Partner API, which records consent, scopes the search to the use location and handles adverse action. Whether your use needs it is a question for your counsel.
Arrest records API: questions developers ask
Is an arrest record the same as a criminal record?
No. An arrest record (a jail_booking) says a person was booked on a charge; the charge's disposition is empty because no court has decided it. A conviction is a court_case record with a filled disposition. Both are returned by the same endpoint, and recordType tells them apart.
How do I check if someone was arrested with the API?
POST /v1/criminal/search with query.lastName, query.firstName, query.dob and, in production, query.state. Read records[] for recordType "jail_booking", confirm matchState is dob_match, then read booking.bookingDate and the charge in cases[].charges[].
How do I know whether the booking is current?
Read lifecycle. presentInLastSweep true and droppedOut false means the person was on the roster at the last check, with lastSeen as the date. droppedOut true means they have left the roster since firstSeen, most likely released. currentStatus is the source's own text and is not always updated, so decide on lifecycle.
Does a booking tell me whether the person was convicted?
Never. Search for the court_case record for the same person and read cases[].charges[].disposition. If there is no court record in the sources on file, say so; do not infer either way.
Which counties does the arrest records API cover?
GET /v1/criminal/sources lists every jurisdiction in the catalog with its scope, status and health.lastSuccessAt, and needs no key. On a search, include: ["coverage"] with a state in the query returns coverage.sources[] for that scope, and counts.sourcesPending counts the sources for that place that are not yet available.
Can I use arrest records for hiring or tenant screening?
Not from this endpoint: every response says legal.notice "Not a consumer report". A hiring, tenant, credit or insurance decision runs as an FCRA order through the FCRA Partner API, and how arrests may be weighed in such a decision is a question for your counsel.
What this page does not cover
- Live re-verification against a county jail portal (
liveblock,202and polling): see cached vs live. - Prison and DOC custody records (
doc_inmate,custodyFacility, release dates): the build an inmate search tool tutorial and the inmate search API page. - Warrants (
recordType"warrant",currentStatus"active_warrant"): see the warrant search page. - Filtering and faceting a result (
filters,include: ["facets"]): the criminal filtering reference.