Criminal Records API · v1.0.0

Date-of-birth matching

One query reaches every kind of birth evidence a record publishes: a full date, a year, or an age. The record tells you which one it had.

What you'll build

The part of a screening integration that decides what birth evidence to send and how to read what came back. Sources differ: a court docket usually publishes a full date, a prison roster a year, a jail roster often only an age at booking, and an exclusion list sometimes nothing at all. The API matches your query against whichever the record has and labels the outcome on every record, so you can build one rule set instead of one per source.

Before you start

  • A test key (os_test_…) on https://sandbox.offendersearch.app.
  • Felony Record, born 1975-06-15, for the full-date, year-only and contradicting-date cases; Possible Match for records with no birth evidence; Sanctioned Provider (with include: ["exclusions"]) for an exclusion-list entry with none.
  • The field reference: Date of birth.
A record with nothing to compare is kept, not dropped. Whether you send dob, birthYear or age, a record that publishes no birth evidence stays in records[] as matchState: "no_dob_age_year", unverified: true. Only a contradiction removes a record. If your logic assumes "I sent a date, so every result is confirmed", it is wrong.

Step by step

  1. Send the strongest evidence you hold. dob (YYYY-MM-DD) when you have it — year and month are derived from it. birthYear (+ optional birthMonth) when you hold only a year. age when that is all a form captured. Send one of them; dob wins if several are present.
  2. Read dob.dobPrecision on each record. full, month_year, year, age, none says what the record itself publishes. It explains why the same query gives dob_match on one record and year_match on the next.
  3. Read matchState and the dob: token. dob:match (full date equal), dob:monthYear, dob:year, dob:ageFromDob (your date against a published age), dob:age (your age against anything), dob:absent.
  4. Test the contradiction path. Send Felony Record with dob: "1990-01-01". The answer is complete with zero records: a dob_mismatch is dropped, never returned.
import requests

API, KEY = "https://sandbox.offendersearch.app", "os_test_your_key"

def run(label, query, **top):
    r = requests.post(f"{API}/v1/criminal/search", headers={"X-API-Key": KEY}, json={"query": query, **top})
    r.raise_for_status()
    env = r.json()
    print(f"{label}: {env['counts']['records']} record(s)")
    for rec in env["records"]:
        print(f"   precision={rec['dob']['dobPrecision']:10} state={rec['matchState']:16} "
              f"conf={rec['matchConfidence']:.2f} basis={rec['matchBasis']}")

run("full date      ", {"lastName": "Record", "firstName": "Felony", "dob": "1975-06-15"})
run("year only      ", {"lastName": "Record", "firstName": "Felony", "birthYear": 1975})
run("wrong date     ", {"lastName": "Record", "firstName": "Felony", "dob": "1990-01-01"})
run("no evidence    ", {"lastName": "Match", "firstName": "Possible", "dob": "1988-04-12"})
run("exclusion, none", {"lastName": "Provider", "firstName": "Sanctioned", "dob": "1988-04-12"}, include=["exclusions"])
What the five runs print
full date      : 1 record(s)
   precision=full       state=dob_match        conf=0.99 basis=lastName:exact firstName:exact dob:match
year only      : 1 record(s)
   precision=full       state=year_match       conf=0.85 basis=lastName:exact firstName:exact dob:year
wrong date     : 0 record(s)
no evidence    : 3 record(s)
   precision=none       state=no_dob_age_year  conf=0.80 basis=lastName:exact firstName:exact dob:absent
   precision=none       state=no_dob_age_year  conf=0.80 basis=lastName:exact firstName:exact dob:absent
   precision=none       state=no_dob_age_year  conf=0.80 basis=lastName:exact firstName:exact dob:absent
exclusion, none: 1 record(s)
   precision=none       state=no_dob_age_year  conf=0.80 basis=lastName:exact firstName:exact dob:absent
The dob object on a full-date record and on an age-only record
// Felony Record — the source publishes a full date
"dob": { "date": "1975-06-15", "birthYear": 1975, "birthMonth": 6, "age": 51, "ageAsOf": "2026-10-01", "dobPrecision": "full" }

// a jail roster that publishes only "AGE 34" at booking (production shape)
"dob": { "date": null, "birthYear": null, "birthMonth": null, "age": 34, "ageAsOf": "2026-09-30", "dobPrecision": "age" }

Against the second record, dob: "1992-04-20" checks whether a 34-year-old observed on 2026-09-30 could have been born on that date (age_match, dob:ageFromDob, with a ±1-year tolerance because an unpublished birthday is consistent with two birth years); age: 34 checks the ages directly (dob:age); birthYear: 1992 has nothing to compare and reads no_dob_age_year.

Reading the result

Query fieldAgainst a `full` recordAgainst `year` / `month_year`Against `age`Against `none`
dobdob_match (dob:match) or droppedyear_match (dob:year, or dob:monthYear when both publish a month) or droppedage_match (dob:ageFromDob, ±1 yr) or droppedno_dob_age_year (dob:absent), kept
birthYear (+birthMonth)year_match or droppedyear_match or droppedno_dob_age_year, keptno_dob_age_year, kept
ageage_match (dob:age, ±1 yr) or droppedage_match (±2 yr) or droppedage_match (±1 yr) or droppedno_dob_age_year, kept
nothingno_dob_age_year on every record

"Dropped" means dob_mismatch: the record is removed before records[] is built and counts.records excludes it. dob.date is a full ISO date or null — never a stub — so you can hand it straight to a date parser.

Edge cases and pitfalls

  • Send `birthYear` as a number. A string "1975" is not type-checked; it compares unequal to every stored year and silently drops every dated record. The same applies to age.
  • `birthMonth` without `birthYear` is ignored. It refines a year match to month+year only when both sides publish a month; a record with dobPrecision: "year" cannot be refined.
  • An invalid `dob` is `422 invalid_dob`, not a no-match: the body must be YYYY-MM-DD (the first ten characters are parsed). Validate the form before sending.
  • A `year_match` is not a confirmation. Two people with the same name and birth year in one county is not rare. Treat it as strong supporting evidence and require a reviewer for consequential outcomes.
  • Live checks can confirm what the record does not publish. On a live search with a full dob, a record with no birth evidence may come back matchState: "dob_match" with dobVerification: "source_confirmed" — the source returned it for your date. source_not_confirmed keeps the record, ranked lower (−0.15). Cached searches never produce either value.
  • Placeholders are scrubbed. 1900-01-01, 0001-01-01, 9999-* and bare 1/1 dates collapse to year or none; they never produce a false dob_match.
  • A sandbox date must be exact. Any other date is a contradiction and returns zero records, exactly as production would.

Production checklist

  • Capture a full date of birth wherever your intake allows; fall back to birthYear, then age.
  • Type birthYear, birthMonth and age as integers before the request leaves your code.
  • Show dob.dobPrecision next to matchState so a reviewer knows what the record could have confirmed.
  • Treat zero records after sending a date as "no record with a consistent birth date", and log the date you sent.
  • Re-run a name-only search with the date once you obtain it; the same records re-score.

Product overview