Understanding a criminal record
A record keeps the source's own words in the top-level fields and the closed classification in `normalized`. Knowing which field is which is most of the job.
What you'll build
A renderer: code that takes one element of records[] and shows a reviewer the right facts in the right order — what kind of record it is, where the person stands today, what the cases and charges say, and how current the copy is. The same reading applies to every record the API returns, whether it came from a cached search, a live check, a batch row or GET /v1/criminal/records/{recordId}.
This guide walks one prison record. For what each record kind is (booking, inmate, case, warrant, registry, exclusion) read Understanding record types; for charges and dispositions read Understanding charges and Understanding dispositions.
Before you start
- A test key (
os_test_…) againsthttps://sandbox.offendersearch.app. - The sandbox person
In Custody, born1979-12-31: one state prison record with a sentence, a parole block, a facility and a projected release date.Arrest Only(1992-04-20) is the contrasting jail booking with no disposition. - The field list you will read against: the Record object and Normalized fields.
recordType is the source's label (open). recordCategory is the closed kind (court|jail|prison|warrant|registry|exclusion|other). currentStatus is the source's status text (open; sources write booked, scheduled, indicted and more alongside the twelve usual values). normalized.custodyStatus is the closed status. Display the open pair; branch on the closed pair.Step by step
- Fetch a record with everything populated. Search for
In Custodywith the date of birth so the match fields are filled in too. - Classify it. Read
recordCategoryfirst, thennormalized.custodyStatus. Together they say "prison record, in custody" without parsing any text. - Read custody and sentence.
currentlyIncarcerated,dateIncarcerated,releaseDate(projected),actualReleaseDate(happened),custodyFacility,sentence,parole,custody,releaseDates. - Walk cases[] and charges[]. Each case is one docket; each charge is one count as the source worded it. The closed reading of the same charge is
normalized.cases[i].charges[j], aligned by index. - Check currency.
sources[].lastCheckedAtsays when the copy was taken;lifecyclesays whether the person is still present at the source.
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": "Custody", "firstName": "In", "dob": "1979-12-31" } }'{
"recordType": "doc_inmate",
"recordCategory": "prison",
"externalId": "SBX-DOC-310045",
"personRef": "prs_0b7e3f9c1d2a4e6b8c90",
"name": { "first": "In", "middle": "Sample", "last": "Custody", "suffix": "" },
"aliases": ["I. S. Custody"],
"dob": { "date": "1979-12-31", "birthYear": 1979, "birthMonth": 12, "age": 46, "ageAsOf": "2026-10-01", "dobPrecision": "full" },
"sex": "M", "race": "White",
"state": "AZ", "county": "",
"address": null,
"currentStatus": "in_custody",
"currentlyIncarcerated": true, "everIncarcerated": true,
"dateIncarcerated": "2019-04-02", "releaseDate": "2031-04-01", "actualReleaseDate": null,
"custodyFacility": "Sample State Correctional Facility",
"sentence": { "length": "12 years", "minDate": "2019-04-02", "maxDate": "2031-04-01", "probation": null, "fine": null,
"dischargeDate": null, "years": 12, "months": null, "goodTimeLaw": null, "truthInSentencing": "Yes" },
"parole": { "eligibilityDate": "2029-10-01", "nextReviewDate": null, "reviewStatus": "Not yet eligible", "lastDecision": null },
"custody": { "custodyClass": "Medium", "description": "General population", "facilityType": "State prison" },
"priorFelony": true,
"matchConfidence": 0.99, "matchState": "dob_match", "matchBasis": "lastName:exact firstName:exact dob:match",
"cases": [
{ "caseNumber": "SBX-CR-2018-007788", "court": "Sample County Superior Court", "county": "Sample", "state": "AZ",
"level": "state", "filedDate": "2018-06-01", "status": "Sentenced",Read it top down: a prison record (recordCategory), the person is in custody now (normalized.custodyStatus, currentlyIncarcerated), one case with one felony count, convicted (normalized.hasConviction), projected release in 2031 (releaseDate) with an earlier DOC projection in releaseDates.projected, and the copy was present at the source on its last check (lifecycle.presentInLastSweep).
Reading the result
| Field | Values | Read as |
|---|---|---|
recordCategory | court · jail · prison · warrant · registry · exclusion · other | The kind of record. The only "kind" field to branch on. |
recordType | open: doc_inmate, court_case, jail_booking, jail, arrest, warrant, healthcare_exclusion, … | The source's label. Show it; never compare exact strings. |
currentStatus | open text, usually one of in_custody, released, on_parole, on_probation, active_warrant, wanted, registered, disposed, absconder, deceased, excluded, unknown | Display only. |
normalized.custodyStatus | the twelve values above, closed | Where the person stands. unknown is a real answer: the source said nothing. |
currentlyIncarcerated / everIncarcerated | booleans | Plain facts. false is not "released"; read currentStatus/actualReleaseDate for that. |
releaseDate vs actualReleaseDate | ISO date or null | Projected or scheduled vs. happened. A releaseDate in the past with actualReleaseDate: null means the source has not recorded the release. |
cases[].charges[] | statute, description, severity, disposition, dispositionDate, offenseDate, plea, counts | The source's words, untranslated. disposition: "" means no outcome text was published — typical on a booking. |
normalized.cases[i].charges[j] | category, severity, severityBasis, outcome, convicted, custodyEvent, notACharge | The closed reading of the same charge, same indexes. normalized: null means the record could not be classified; treat outcome as unknown. |
lifecycle | firstSeen, lastSeen, presentInLastSweep, droppedOut, droppedOutDetected, dropInterpretation, sourceLastUpdated | droppedOut: true records are still returned. dropInterpretation guesses why: released (jail), source_removed (court/registry), reinstated (exclusion), unknown. |
personRef | prs_ + 20 hex | Stable per source per person; what a clearance names. externalId is the source's own key, unique only within sources[].code. |
Edge cases and pitfalls
- The three empties.
""= the source published no text for a string slot (name.middle,statute,disposition,plea,caseNumber).null= a typed absence: dates, numbers,sex,race,county, and whole optional objects (address,sentence,parole,custody,exclusion,normalized).[]= none on file (aliases,cases,charges,sources,vehicles,otherDetails). Never collapse them into one "missing". - Some keys are present only when they apply.
cases[].sentenceappears only when the case has one;cases[].chargesPending: trueappears only when the case page was never fully retrieved (thenchargesis[], meaning "not retrieved", not "no charges");matchedLocationsonly whenlocationswas sent;clearanceonly whensubjectIdnamed thispersonRef. - A booking is not a conviction.
Arrest Onlyhas one charge withdisposition: "",cases[].status: "Booked"andbooking.*populated.normalized.hasConvictionisfalseornullthere. Arrests vs. charges vs. convictions. - Dates are ISO when they parse, otherwise the source's text. The core dates (
dob.date,filedDate,dispositionDate,dateIncarcerated,releaseDate) areYYYY-MM-DDornull. The detail groups (booking.bookingDate,warrant.issuedDate,releaseDates.*,cases[].nextHearing.date,events[].date,lifecycle.sourceLastUpdated) fall back to the published text when it does not parse. Parse defensively. - `dob.date` is never a placeholder.
1900-01-01,0001-01-01and1/1stubs collapse todobPrecision: "year"or"none"withdate: null. You will never see a fabricated day. - `sex`, `race`, `physical.hairColor` are the source's words; the NCIC-style codes live in
normalized.sex,normalized.race,normalized.hair,normalized.eyes. - `GET /v1/criminal/records/{recordId}` returns the bare record with
matchState: null,matchConfidence: 0.0anddobVerification: null— it scores nothing. The path value is the record'sexternalId; send?source=with itssources[].code.
Production checklist
- Branch on
recordCategoryandnormalized.custodyStatus; renderrecordTypeandcurrentStatusas labels. - Read charge outcomes from
normalized.cases[i].charges[j].outcome/convicted, with a fallback of "unknown" whennormalizedisnull. - Distinguish
"",nulland[]in your model; addchargesPendinganddroppedOutto what a reviewer sees. - Link the reviewer to
sources[].recordUrland showsources[].lastCheckedAt. - Key your own de-duplication on
personRefwithin asources[].code, never on the name.