Developer docs · v1.0.0

Court records API tutorial: cases, charges and dispositions

Court cases are one layer of the criminal records search. This tutorial makes the request, walks the case object field by field, reads a disposition, filters and sorts by date, scopes to a county, pages, and renders a PDF, all against the sandbox.

Updated November 3, 2026

What the court records API returns

There is no separate court endpoint. Court cases are one record kind inside the criminal records API: POST /v1/criminal/search answers with records[], and each record carries a recordType. A court record is recordType: "court_case"; beside it in the same answer you may see jail_booking (arrest stage, no disposition), doc_inmate (custody), warrant, wanted_fugitive, registry and healthcare_exclusion.

The dataset is criminal court records, the same one the court records search product page describes. Civil suits, family and probate matters and traffic citations are not in it. A court record carries one or more cases[], and a case carries charges[], each with its own disposition.

Every search answer also carries a legal.notice line: "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.)." Read it as a statement about this endpoint. A hiring or housing decision belongs on the FCRA Partner API, where the same records arrive inside an order with consent and notices.

Python and Node.js SDKs ship with the first registry release; see SDKs. Until then the raw HTTP calls below are the integration. A test key is free from the dashboard, nothing is billed on the sandbox, and the sandbox allows 60 calls a minute and 2,000 a day per key.

Court records API tutorial: the first request

query.lastName is the one required field; a query without it is 422 guard_unbounded_query. Send firstName and a full dob whenever you have them, because the date of birth is what turns a name match into an identification (matchState: "dob_match"). The sandbox subject Felony Record, born 1975-06-15, returns exactly one court case. The same name without a dob returns the same case with matchState: "no_dob_age_year" and unverified: true; the same name with dob: "1990-01-01" returns nothing, because a record whose published date of birth contradicts yours is dropped rather than returned.

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": "Felony", "lastName": "Record", "dob": "1975-06-15"}}'

The answer is status: "complete", counts.records: 1, one record with recordType: "court_case", matchConfidence: 0.99, matchState: "dob_match" and matchBasis: "lastName:exact firstName:exact dob:match". Swap in Misdemeanor Record, 1988-11-02, for a second court case: one misdemeanor theft charge, disposition: "Guilty", plea: "No contest", and a sentence.fine of 250.0. Any other name, including Jordan Rivera, 1988-04-12, answers complete with zero records.

The court case object in a court records API response

This is records[0].cases[0] from the sandbox answer, unedited. Every key is always present; a value the source did not publish is "", null or [], never missing and never invented.

cases[0] for Felony Record, 1975-06-15
{
  "caseNumber": "SBX-CR-2015-000123",
  "court": "Sample County Court of Common Pleas",
  "county": "Sample",
  "state": "OH",
  "level": "county",
  "filedDate": "2015-03-02",
  "status": "Closed",
  "caseType": "FELONY",
  "charges": [
    {
      "statute": "SAMPLE 2911.12",
      "description": "Burglary",
      "severity": "felony",
      "disposition": "Guilty",
      "dispositionDate": "2015-09-14",
      "offenseDate": "2015-02-20",
      "plea": "Guilty",
      "counts": 1
    },
    {
      "statute": "SAMPLE 2913.02",
      "description": "Theft",
      "severity": "misdemeanor",
FieldWhat it holds
caseNumber, court, county, state, levelThe docket number and the court that holds it; level is county, state or federal.
filedDate, status, caseTypeWhen the case was filed, the court's own status word (Closed, Open…), and its class as published (FELONY, MISDEMEANOR).
charges[]One entry per count: statute, description, severity (felony, misdemeanor, …), disposition, dispositionDate, offenseDate, plea, counts.
events[]The docket timeline: date, type, description, outcome. Arraignments, hearings, pleas.
sentencelength, minDate, maxDate, probation, fine, dischargeDate, years, months. null when nothing was imposed or published.
judge, attorney, prosecutor, division, nextHearingCourt metadata when the source publishes it.

Around the case sit the person-level keys you already know from the criminal record object: name, aliases[], dob (with dobPrecision), state, county, currentStatus, sources[] (the code, level, recordUrl, lastCheckedAt and live of each source), lifecycle, identifiers and recordCategory: "court".

Reading a disposition in court records

A disposition lives on the charge, not the case, because one case can end two ways. The sample case does exactly that: the burglary count is disposition: "Guilty" with plea: "Guilty", and the theft count on the same docket is disposition: "Dismissed" with an empty plea. Both carry the same dispositionDate, and the second docket event explains why: *Guilty plea to count 1; count 2 dismissed*. A program that reads cases[0].charges[0].disposition and stops has read half the case.

The rule for deciding whether a record is a conviction is therefore: walk every charges[] entry of every cases[] entry, and read each disposition. The dispositions guide lists the words courts use and which mean a conviction.

A missing disposition stays empty. Many dockets publish a charge before the court has ruled on it. That charge arrives with disposition: "" and dispositionDate: null. The API never fills the gap with a guess, and your code must not either: treat an empty disposition as "pending or unpublished", not as "not guilty" and not as "guilty".

If you want a machine-readable answer instead of the court's own words, read the normalized block described below: outcome: "convicted" and convicted: true on the burglary count, outcome: "dismissed" and convicted: false on the theft count, and outcome: "unknown" with convicted: null when the published text decides nothing.

Sorting and filtering court records by date and outcome

Three top-level request keys narrow and order the answer. filters is ANDed across keys and ORed within a list; sort takes relevance (default), offenseDate, filedDate or dispositionDate, and a date sort puts the record's latest date of that kind first with undated records last; include: ["normalizedDetail"] adds a rule to every normalized charge so an audit can see which rule decided the outcome. sort is applied when the request sends filters or locations.

Court convictions only, felonies, filed since 2010, newest disposition first
{
  "query": { "firstName": "Felony", "lastName": "Record", "dob": "1975-06-15" },
  "filters": {
    "recordCategory": ["court"],
    "severity": ["felony"],
    "outcome": ["convicted"],
    "filedDate": { "from": "2010-01-01", "to": "2026-10-31" }
  },
  "sort": "dispositionDate",
  "include": ["normalizedDetail", "facets"],
  "page": 1,
  "perPage": 50
}
FilterTestsValues
recordCategorythe recordcourt, jail, prison, warrant, registry, exclusion, other
severityeach chargefelony, misdemeanor, infraction
outcomeeach chargeconvicted, pending, dismissed, nolle_prosequi, acquitted, deferred_or_diverted, vacated, warrant, transferred, closed_unspecified, administrative
convictedOnlyeach chargetrue keeps records with at least one convicted charge
chargeCategories, statute, chargeTexteach chargea category list, a statute prefix such as 22.02, or text in the description
filedDate, dispositionDate, offenseDatethe record's latest date{ "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" }, at least one bound
lookbackYearsthe record's latest date1–100
unknownevery classified filterkeep (default) or exclude for values that could not be classified

Every record also carries a normalized block, with or without filters: clean classes computed from the fields beside it while the court's own words stay untouched. With normalizedDetail each charge adds its rule:

records[0].normalized with include: ["normalizedDetail"] (the shape; values follow the reference enums)
"normalized": {
  "rulesVersion": 1,
  "quality": "ok",
  "recordCategory": "court",
  "custodyStatus": "on_probation",
  "cases": [
    {
      "stage": "closed",
      "caseClass": "felony",
      "charges": [
        { "category": "burglary", "notACharge": false, "severity": "felony", "severityBasis": "published",
          "degree": null, "outcome": "convicted", "convicted": true, "custodyEvent": null,
          "rule": "disposition_text:guilty" },
        { "category": "theft", "notACharge": false, "severity": "misdemeanor", "severityBasis": "published",
          "degree": null, "outcome": "dismissed", "convicted": false, "custodyEvent": null,
          "rule": "disposition_text:dismissed" }
      ]
    }
  ]
}

A record a filter removes is counted in counts.filteredOut; a record kept because its value was unknown is counted in counts.unknownKept. The filtering reference has every key, and the normalized fields reference every class and the rule names.

Scoping court records to a county or state

Court records are kept by the court that holds the docket, so a county is the natural unit. Put state and county inside query to scope a cached search to one county; state alone scopes to the state. Send either the flat state / county keys or a locations[] list (up to ten places), never both, or the request is 422 ambiguous_location.

# Cached search, scoped to one county
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": "Misdemeanor", "lastName": "Record", "dob": "1988-11-02",
                 "state": "TX", "county": "Sample"}}'

# Which court sources exist? No key needed. Each row: id, name, covers[], scope, recordType, status, health
curl https://api.offendersearch.app/v1/criminal/sources

# Re-verify one county source live ($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": "Misdemeanor", "lastName": "Record", "dob": "1988-11-02", "state": "TX"},
       "live": {"jurisdictions": ["TX-BEXAR"]}}'

GET /v1/criminal/sources needs no key and lists every jurisdiction: its public id (the code you pass to live.jurisdictions), a human name, the states it covers, its scope (county, state, federal), its recordType in words ("Court records", "County jail roster", "Exclusion list"), whether it is live or pending, and a health object with lastSuccessAt and ageSeconds. Filter that list on recordType to see which court sources exist before you promise a county to a customer.

A cached search is answered from the continuously updated dataset and sends no live block. To re-verify a county source at the moment of the search, add live.jurisdictions with its code, or live.scope: "matched" to re-check every jurisdiction that produced a cached hit. Live re-verification costs $0.02 per completed source. With two or fewer sync-eligible sources the answer comes back inline; with more, the response is 202 with status: "pending", mode: "async" and a poll path, GET /v1/criminal/searches/{searchId}. The cached vs live guide covers both paths.

Paging court records and rendering a PDF report

page and perPage are top-level request keys, not query keys, and perPage runs from 1 to 200; any other value is 422 invalid_page. The answer carries page, perPage, totalPages and counts.records (the total, which does not change as you page) beside counts.recordsReturned (this page). Walk page = 1 … totalPages.

# Page 2 of 200-record pages. page and perPage are TOP LEVEL, not inside query.
curl -X POST https://sandbox.offendersearch.app/v1/criminal/search \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": {"lastName": "Record", "state": "OH"}, "page": 2, "perPage": 200}'

# Render the search you already ran as a PDF (report charge only; no second search)
curl -X POST https://sandbox.offendersearch.app/v1/criminal/report \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"searchId": "srch_sbx_cr_felony", "viewerName": "Taylor Brooks",
       "purpose": "Visitor screening", "reference": "file-4471"}' \
  --output court-records.pdf

POST /v1/criminal/report renders a PDF. Send the searchId of a search you already ran on your account and only the report is billed; send an inline query instead and the search runs first. viewerName, viewerEmail, purpose and reference are printed at the top of the document as a record of who ran it; they are not an access gate.

Searches cost $0.15 a call, $0.11 past 2,000 calls in a month, with 25 free calls and no card. A repeated POST is a second billed search, so persist the searchId and re-render from it rather than resubmitting.

Step by step: how to get court records via API

  1. Mint a test key and point at the sandbox. Create an os_test_ key in the dashboard, export it as OFFENDERSEARCH_API_KEY, and send every call to https://sandbox.offendersearch.app. Nothing is billed there.
  2. Search by last name, first name and date of birth. POST /v1/criminal/search with query.lastName (required), query.firstName and a full query.dob. Try Felony Record, 1975-06-15, then Misdemeanor Record, 1988-11-02.
  3. Keep the court layer. Keep records whose recordType is court_case, or send filters.recordCategory: ["court"] so the answer carries only court records.
  4. Walk every charge and read its disposition. For each cases[] entry, for each charges[] entry, read disposition, dispositionDate, plea and counts. Treat an empty disposition as pending or unpublished, never as an outcome.
  5. Filter and sort when the docket is long. Send filters (severity, outcome, filedDate, lookbackYears), set sort to filedDate or dispositionDate, and read normalized.cases[].charges[].outcome for a machine-readable class.
  6. Scope to a county and re-verify it live when it matters. Put state and county in query; check GET /v1/criminal/sources for the court sources that exist; add live.jurisdictions to re-check one source at the moment of the search.
  7. Page, then render a PDF. Walk page 1 to totalPages with perPage up to 200. Pass the searchId to POST /v1/criminal/report for a PDF of the search you already ran.

Court records API questions developers ask

Is there a separate court records endpoint?

No. Court cases are recordType: "court_case" records inside POST /v1/criminal/search. Send filters.recordCategory: ["court"] to receive only the court layer.

How do I know whether a court record is a conviction?

Read cases[].charges[].disposition on every charge, or normalized.cases[].charges[].outcome for the class (convicted, dismissed, pending, …). One case can hold a guilty count and a dismissed count side by side; the sample case does.

What does an empty disposition mean?

The court has not published one: the charge may be pending or the docket may not carry it. The API leaves disposition as "" and dispositionDate as null rather than guessing. normalized says outcome: "unknown".

Does the court records API include civil, family or traffic cases?

No. The dataset is criminal court records, with jail, custody, warrant, registry and exclusion-list layers beside it. A civil suit, a divorce or a traffic citation is not returned.

How do I find out which counties have court records?

GET /v1/criminal/sources, with no key, lists every jurisdiction with its recordType in words and its status. Rows whose recordType is court records are the court sources; GET /v1/criminal/jurisdictions lists the codes by state and county.

Can I use court records from this endpoint for a hiring decision?

The response says legal.notice: "Not a consumer report…". An employment, tenant, credit or insurance decision runs as an order on the FCRA Partner API for approved partners; ask your counsel which applies to you.

What this page does not cover