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_…) onhttps://sandbox.offendersearch.app. Felony Record, born1975-06-15, for the full-date, year-only and contradicting-date cases;Possible Matchfor records with no birth evidence;Sanctioned Provider(withinclude: ["exclusions"]) for an exclusion-list entry with none.- The field reference: Date of birth.
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
- Send the strongest evidence you hold.
dob(YYYY-MM-DD) when you have it — year and month are derived from it.birthYear(+ optionalbirthMonth) when you hold only a year.agewhen that is all a form captured. Send one of them;dobwins if several are present. - Read dob.dobPrecision on each record.
full,month_year,year,age,nonesays what the record itself publishes. It explains why the same query givesdob_matchon one record andyear_matchon the next. - 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. - Test the contradiction path. Send
Felony Recordwithdob: "1990-01-01". The answer is complete with zero records: adob_mismatchis 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"])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// 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 field | Against a `full` record | Against `year` / `month_year` | Against `age` | Against `none` |
|---|---|---|---|---|
dob | dob_match (dob:match) or dropped | year_match (dob:year, or dob:monthYear when both publish a month) or dropped | age_match (dob:ageFromDob, ±1 yr) or dropped | no_dob_age_year (dob:absent), kept |
birthYear (+birthMonth) | year_match or dropped | year_match or dropped | no_dob_age_year, kept | no_dob_age_year, kept |
age | age_match (dob:age, ±1 yr) or dropped | age_match (±2 yr) or dropped | age_match (±1 yr) or dropped | no_dob_age_year, kept |
| nothing | no_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 toage. - `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 backmatchState: "dob_match"withdobVerification: "source_confirmed"— the source returned it for your date.source_not_confirmedkeeps the record, ranked lower (−0.15). Cached searches never produce either value. - Placeholders are scrubbed.
1900-01-01,0001-01-01,9999-*and bare1/1dates collapse toyearornone; they never produce a falsedob_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, thenage. - Type
birthYear,birthMonthandageas integers before the request leaves your code. - Show
dob.dobPrecisionnext tomatchStateso 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.