Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Offendersearch Records API
version: 1.0.0
description: "National sex-offender search across all US states and territories, unified behind one API. Synchronous by default (call and wait) for interactive use; an asynchronous mode returns a job handle and is built for batch and high-volume work. Full offenders.io field parity plus scored matches, per-source provenance, freshness options, DOB/age match-state fidelity, and a consolidated verification-report PDF (`POST /v1/report`) with a source citation on every record. Criminal records land later as an additive `recordType` — the contract does not change.\n\n## Authentication\nMost customer endpoints authenticate with an API key sent in the `X-API-Key` header (`ApiKeyAuth`). The primary synchronous search additionally accepts a signed **session token** (`Authorization: Bearer <token>`) so the dashboard console can run searches on behalf of a signed-in user. The offenders.io compatibility endpoint accepts the key three ways for drop-in parity: `X-API-Key`, `Authorization: Bearer <key>`, or `?key=<key>`. Account/dashboard endpoints (`/v1/account`, `/v1/keys`, `/v1/usage`, ...) require a session token. Internal ops endpoints require the separate `X-Admin-Key` credential.\n\n## Freshness & billing\n`freshness` is a per-request parameter with two values — `daily` (the DEFAULT when omitted) and `weekly`.\n\n* **`daily`** is the freshest data we publish. Our sweep of every registry runs\n on a daily cycle, so a `daily` answer is assembled from the newest snapshot we\n hold of each one — in practice, almost real time. Ask for it when currency is\n the point. It bills the **+$0.01/call daily-freshness surcharge** on top of the\n base per-call rate (admin-overridable per customer).\n* **`weekly`** is also fresh. Identity is essentially identical to `daily` — the\n same people, names, aliases, offenses, addresses and photos — and what can lag\n is only the most recent movement, a registration or an address change from the\n last day or so. Right for bulk screening and periodic re-screens. **No\n surcharge.**\n\n\n**`freshness` describes the answer; it does not filter it.** No registry is ever withheld from a result because of when it was last swept: every registry covering the query contributes, from the newest snapshot we hold of it. `status` never becomes `partial` for a snapshot age, and `warnings[]` never carries a staleness sentence.\n\n**★ EVERY DATE IS ISO-8601 — BREAKING CHANGE, 2026-08-04.** Every date-shaped field in a record now comes back as ISO-8601: `dob`, the four `offense.*Date` fields, and the five `stateData` date fields. They used to be the registry's own string passed through byte-for-byte, so a single response could carry `\"2003-03-31\"`, `\"10/11/1988\"`, `\"11-29-1987\"` and `\"Aug. 10, 1987\"` in the same key — `offense.offenseDate` was ISO on only 45.4% of its populated values, in seven distinct shapes. **If you wrote a lenient parser or a per-state format table, you can delete it; if you compared these values as raw strings, or stored them in a text column and matched on it, those comparisons will change once.**\nThree rules govern the new values, and the third is the one to read carefully:\n1. A full date is `YYYY-MM-DD`. 2. **A partial date stays partial.** A registry that publishes only a month or\n only a year yields `YYYY-MM` or `YYYY` — we never invent a day. The companion\n `datePrecision` object on `offense` and `stateData` names the precision\n explicitly, exactly as `dobPrecision` has always done for `dob`.\n3. **Nothing is discarded.** A value we cannot read as a date — a crime\n description in an offence-date cell, a `registrationEnds` of \"Life\" — is NOT\n served as a fake date and NOT silently blanked. The field is `\"\"`, the\n precision is `unparseable`, and the registry's literal text is preserved in\n the `datesAsPublished` object beside it.\n\n`source.scrapedAt` / `.lastCheckedAt` / `.sourceUpdatedAt` were already ISO-8601 timestamps and are unchanged. `dob` was already `YYYY-MM-DD` and is unchanged.\n\n**There is no date FILTER on the offense or stateData dates.** `dob` is the one date you can narrow a query on; read the rest off the record.\n**Provenance is never traded for normalisation.** The registry's own text is kept beside the normalised value, not overwritten by it, which is why `datesAsPublished` still carries what the source actually printed — including the 38,818 `registrationEnds` cells that hold a registration DURATION (\"15 Years\", \"Lifetime\") rather than a date.\n\n**Where currency is published — and where it is not.** Per record, `record.source.scrapedAt` is the moment we ingested the snapshot that record came from. Per registry, `GET /v1/sources` publishes `health.lastSuccessAt` and `health.ageSeconds` live and without an API key. Both are things you ask for. Neither is attached to a search answer: `sourceStatus[]` carries no age fields, because an answer that contains every matching record does not need a caveat about our sweep schedule.\n\n★ **Freshness and completeness are different questions, and only one of them makes an empty result unsafe.** `freshness` says how OLD an answer is; it says nothing about whether the answer is WHOLE. A registry swept ten minutes ago can still fail to be searched to the end — a deadline, or a candidate set larger than one search may examine — and then a `0` from it means UNKNOWN, not NO MATCH. That is `counts.sourcesIncomplete` / `sourceStatus[].incomplete`, it is unrelated to freshness, and it is the one signal that must never be ignored. See API-CONTRACT.md §5.0.\n\n**Migrating from a pre-2026-08-04 integration.** `counts.sourcesStale`, `counts.sourcesDegraded`, `counts.sourcesOmitted`, `counts.recordsFromStaleSources`, `freshnessDetail`, and `sourceStatus[].freshnessSatisfied` / `.degraded` / `.omitted` / `.ageSeconds` / `.lastSuccessAt` were removed and are no longer emitted. If you were gating on any of them, gate on `counts.sourcesIncomplete` instead — it asks whether your ANSWER is whole, which is the question those keys were being used to ask. `onStale` is deprecated but still accepted, so an older request body keeps working.\n\nBilling is required for every account; the ONLY free call is one scoped exclusively to statutorily non-commercial jurisdiction(s) (e.g. state=CA), which has nothing commercially billable. **Batch billing is per-search:** `POST /v1/batch` is one HTTP request but each search in it is billed as its own search (per-row freshness included) — a batch of 100 = 100 billable searches. The consolidated verification report (`POST /v1/report`) is a separate, callable endpoint any valid key may use (no per-key entitlement), billed **+$0.02 per document** (admin-overridable per customer). A separate `proof` add-on also carries an extra per-document charge and requires billing (402 otherwise); it is NOT the customer verification report.\n"
servers:
- url: https://api.offendersearch.app
security:
- ApiKeyAuth: []
tags:
- name: Records
paths:
/v1/records/{recordId}:
get:
operationId: getRecord
summary: Fetch a single normalized record by recordId or uuid
description: 'Resolves a record by EITHER identifier a search published for it: our `recordId` (`rec_…`) or the registry''s own `uuid`. DATABASE-BACKED since 2026-08-04 — it previously read only an in-memory index local to one API container, keyed on `recordId` alone, so a `uuid` never resolved and a `recordId` resolved only when the follow-up request happened to reach the same container as the search.
A `404` means THIS IDENTIFIER IS NOT CURRENT — never that the person is unregistered. Registries re-issue their ids (KY, MO, OR, AZ, IA and WV each re-issued their entire registry within eight days to 2026-08-04; CA re-issued ~104,000). Re-search by name and state, then store the new `uuid`.
An identifier carrying a byte no id of ours can hold — a NUL or any other control character — is also a `404`. Until 2026-08-05 `/v1/records/%00` and `/v1/records/a%00b` returned **500**, the only 5xx reachable from ~60 hostile inputs, because the NUL reached a database parameter. A 5xx is a claim that WE failed, and for a malformed id that claim is false. `%01` and `%25` always answered 404; now every impossible id answers the same way.
A `409` means the id is AMBIGUOUS. A `uuid` is unique only together with its jurisdiction — `20059` identifies a Florida, a Pennsylvania and a Wisconsin registrant, and 55,893 ids are shared across jurisdictions (147,624 records). This endpoint returns one record and will not guess which you meant; use `GET /v1/compat/sexoffender?uuid=…&state=XX`, which returns every match with its jurisdiction, or use `recordId`, which is namespaced per jurisdiction and does not collide.
'
parameters:
- name: recordId
in: path
required: true
description: The `recordId` (`rec_…`) or the registry `uuid` from a search result.
schema:
type: string
example: rec_ab12cd34ef56gh78ij90
responses:
'200':
description: The record
content:
application/json:
schema:
$ref: '#/components/schemas/Record'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'409':
description: 'The identifier matches records in more than one jurisdiction. `detail` names them. Disambiguate with the compat endpoint''s `state`, or use `recordId`.
'
tags:
- Records
components:
schemas:
Error:
type: object
description: Standard FastAPI error body.
properties:
detail:
type: string
description: Human-readable error message.
example: Missing or invalid API key. Send it in the X-API-Key header.
SourceRef:
type: object
description: Provenance stamp — mandatory on every record's primary source.
properties:
jurisdiction:
type: string
description: Jurisdiction code, e.g. IL.
example: IL
registryName:
type: string
recordUrl:
type: string
format: uri
description: Link to the underlying record.
scrapedAt:
type: string
format: date-time
nullable: true
description: createdAt basis — when we first recorded it.
lastCheckedAt:
type: string
format: date-time
nullable: true
sourceUpdatedAt:
type: string
format: date-time
nullable: true
description: updatedAt basis — when the source last changed the record.
Name:
type: object
properties:
first:
type: string
middle:
type: string
last:
type: string
suffix:
type: string
full:
type: string
description: 'Computed: first middle last suffix, joined.'
Address:
type: object
properties:
type:
type: string
default: residence
description: 'Address kind. This is an OPEN vocabulary, not a closed enum — treat it as a string with a common case. Values observed in production, by frequency: `residence` (~85%), `employment` (~8%), `other` (~4%), `incarceration`, `H`, `O`, `school`, `transient`, `RL`. The single-letter values are raw registry codes that are not yet normalized. Match `residence` explicitly and bucket everything else rather than switching exhaustively.
'
example: residence
line1:
type: string
city:
type: string
county:
type: string
state:
type: string
zipcode:
type: string
lat:
type: number
nullable: true
description: Populated only for GIS/geocoded sources.
lng:
type: number
nullable: true
StateData:
type: object
description: 'Parity: offenders.io extensive stateData (retained only for include=stateData/extensive).'
properties:
stateOffenderId:
type: string
status:
type: string
description: 'Where the registrant stands with the registry, in the registry''s own words, or `""`. This is the key that answers *is this person still at liberty?* — and it is distinct from `offense.riskLevel`, which carries a risk classification. Free text, per state: **Florida (FDLE)** publishes nine values — `"Released - Subject to Registration"`, `"Confinement"`, `"Supervised - FL Dept of Corrections"`, `"Supervised - US Probation"`, `"Supervised - FL Dept of Juvenile Justice"`, `"Deported"`, `"Deceased"`, `"Absconded"`, `"Civil Commitment"` — while other states use their own (`"Compliant"`, `"Non-Compliant"`, …). **Match on the exact strings a given state publishes; there is no cross-state enum.** Two of these conditions also have booleans you can read without string matching: `flags.absconder` and, where published, `stateData.incarcerationStatus`.
★ **Georgia (GBI)** publishes this instead of an address for registrants it records as moved out of state, deceased, homeless, incarcerated outside Georgia, or out of the country — e.g. `"*** MOVED OUT OF STATE ***"`, `"*** INCARCERATED - OUT OF STATE ***"`, `"ABSCONDER (Address Unknown)"`. Georgia''s coverage is built from GBI''s per-registrant pages as well as its bulk export, so these registrants are returned like any other; read `stateData.status` for why `addresses[]` may be empty on them, and note that address- and radius-filtered queries cannot reach them.
'
designation:
type: string
description: 'The registry''s statutory CLASSIFICATION of the offender, verbatim — e.g. `"Sexual Predator"`, `"Sexual Offender"`, `"Juvenile Sexual Offender"` — or `""`. Distinct from a risk tier. Where a registry distinguishes a predator class, `flags.predator` mirrors it as a boolean.
'
registrationEnds:
type: string
description: 'When the registration TERM ends. ISO-8601 (`YYYY-MM-DD` / `YYYY-MM` / `YYYY`) or `""`. ★ READ `datePrecision.registrationEnds` BEFORE YOU TRUST AN EMPTY VALUE HERE. 49,961 records publish something in this field that is not a date at all but a DURATION or a lifetime marker — Wisconsin "15 Years" / "25 Years" / "Life" (25,218), Oklahoma "Lifetime" (6,606), North Dakota "LIFETIME" (742). Those come back as `""` with precision `unparseable` and the literal text in `datesAsPublished`, because a date-typed key must hold a date or nothing. They are NOT missing data, and reading `""` here as "no end date on file" would be wrong for exactly those registrants — check `datesAsPublished.registrationEnds`, and see also `isLifetimeRegistration` and `registrationDuration`.
'
datePrecision:
type: object
additionalProperties:
type: string
enum:
- exact
- year_month
- year
- none
- unparseable
description: 'How much of each date this registry published, one entry for every date key on this object — `registrationEnds`, `lastVerificationDate`, `addressVerificationDate`, `sentenceCompletionDate`, `registrationStarts` — always all five, always present. Values as documented on `Offense.datePrecision`.
'
datesAsPublished:
type: object
additionalProperties:
type: string
description: 'The registry''s ORIGINAL text for any date key on this object whose published form differs from what we now emit; `{}` when none do. This is where "Life", "15 Years" and "Lifetime" live after normalisation — they are preserved, not discarded.
'
verificationRequirement:
type: string
lawAgency:
type: string
judgmentOfConvictionUrl:
type: string
format: uri
vehicles:
type: array
items:
type: object
additionalProperties: true
properties:
plate:
type: string
description: 'Plate as printed, usually state-prefixed: "UT F814NW".'
make:
type: string
description: '"Ford", "Kawasaki", "General Motors".'
model:
type: string
description: '"F150", "Motorcycle" — free text, sometimes a class rather than a model.'
year:
type: string
description: 'STRING, not an integer: "2018".'
color:
type: string
description: '"White", "Black Green (Light)" — registry vocabulary, not normalized.'
description: 'Vehicles the registrant has reported to the registry. Keys are not guaranteed: the five above are what the OffenderWatch cluster prints, and other registries publish different ones — always read defensively. `[]` is AMBIGUOUS and carries three different facts that the response cannot separate: this person reported no vehicle, this registry does not publish vehicles at all, or the detail page has not been fetched for this record yet. Treat `[]` as "no vehicle known", never as "no vehicle owned". Populated in 22 jurisdictions; among records that HAVE a detail page, roughly 27-69% carry at least one (FL 27%, PA 44%, OH 58%, LA 61%).
'
photos:
type: array
items:
type: object
additionalProperties: true
complianceStatus:
type: string
description: e.g. compliant | non-compliant.
isLifetimeRegistration:
type: boolean
nullable: true
description: True when registration is for life.
lastVerificationDate:
type: string
description: 'When the offender last verified, ISO-8601 or `""`. ★ Format changed 2026-08-04: it was ISO on 85.9% of its populated values and `MM/DD/YYYY` on the rest, despite this description having claimed "ISO date" all along. It is ISO now.
'
addressVerificationDate:
type: string
description: 'When the address was verified (e.g. KS "Current as of"), ISO-8601 or `""`. ★ Format changed 2026-08-04 — previously ISO on 96.2% of populated values, `MM/DD/YYYY` on the rest.
'
incarcerationStatus:
type: string
description: e.g. incarcerated | released (e.g. SD IsInJail).
comments:
type: string
description: Free-text registry remarks.
criminalHistory:
type: string
description: Narrative / prior-history summary.
adjudication:
type: string
description: e.g. adult | juvenile.
registrationDuration:
type: string
description: e.g. 10 years | lifetime.
skinTone:
type: string
description: IA skin-tone descriptor (e.g. light | medium | dark).
residencyRestriction:
type: string
description: IA residency constraint (free text).
employmentRestriction:
type: string
description: IA employment constraint (free text).
exclusionZones:
type: array
items:
type: string
description: IA geographic zones the offender is barred from.
district:
type: string
description: DC police district.
psa:
type: string
description: DC Police Service Area.
quadrant:
type: string
description: DC quadrant (NW | NE | SW | SE).
birthCity:
type: string
description: Place of birth — city (WY DCI prints a 'Place Of Birth' row on every detail page).
birthState:
type: string
description: Place of birth — state.
birthCountry:
type: string
description: Place of birth — country.
sentenceCompletionDate:
type: string
description: 'Date the SENTENCE completes (OK''s "Completion of Sentence" row, present on roughly half of Oklahoma profiles). Distinct from `registrationEnds` (the registration TERM) and from `offense.releaseDate` (release from custody). ISO-8601 or `""`. ★ BREAKING 2026-08-04 — Oklahoma publishes this as `MM-DD-YYYY`, so 100% of its populated values changed shape.
'
registrationStarts:
type: string
description: 'Date registration began — the partner of `registrationEnds`. ISO-8601 or `""`. ★ EXPANDED 2026-08-09 from one jurisdiction to sixteen: Oklahoma ("Orig Reg Date"), Virginia ("Initial Registration Start Date"), Pennsylvania ("Registration Start"), Kansas ("Registered Since"), Wisconsin, North Carolina, Georgia, Illinois, Indiana, Delaware, Alaska, Puerto Rico, New Mexico, Wyoming, Guam and the District of Columbia. It is the FIRST registration, not the most recent one: registries that publish only a latest-registration or address event date (e.g. Oregon, Texas) leave this `""` rather than filling it with a different fact. ★ BREAKING 2026-08-04 — was `MM-DD-YYYY` on 100% of its populated values.
'
professionalLicenses:
type: array
items:
type: string
description: 'Professional / occupational licences the registrant holds, as free text exactly as the state prints them. Prose, not a structured licence table — do not parse. `[]` means the registry lists none for this person, or does not publish the section. ★ EXPANDED 2026-08-09: **Utah** (`Professional Licenses` tab — type, and often an expiry, e.g. `CDL Class A - Expires: 04-13-2030`) and **Hawaii** (`License Type` — the occupation alone, e.g. `PHYSICIAN`, `REAL ESTATE SALESPERSON`, `ELECTRICIAN JOURNEYMAN INDUSTRIAL`; no number and no expiry is published, so none is reported).
'
shoeSize:
type: string
example: 10½
description: 'Shoe size as Texas DPS records it, in the physical-description block beside height, weight, hair and eye colour. **Texas only** — `""` on every other jurisdiction, always, so an empty string here cannot be read as "this person has no shoe size on file" outside TX. A STRING, not a number: half sizes use the `½` character (`"09½"`) and leading zeros are kept as published. DPS''s explicit `UNKNOWN` literal is normalised to `""`. Fill: 72,289 of 107,939 present Texas registrants (67.0%), from the twice-weekly bulk export, so there is no detail-fetch lag.
'
build:
type: string
example: Large
description: 'Body build as the registry prints it, in the physical-description block beside skin tone, hair and eye colour. **Minnesota only**, and within Minnesota only its DOC community-notification cohort — 381 of 1,450 present Minnesota records (26.3%; 88.4% of that cohort''s 431). The BCA non-compliant cohort served under the same `MN` code publishes no build at all. FREE TEXT, not an enum: the live vocabulary measured over the whole cohort on 2026-08-04 is Medium 209, Large 77, Stocky 27, Slender 25, Small 22, Husky 13, Lean 7, Muscular 1. Minnesota''s own `Unknown` literal is normalised to `""`. An empty string means "this registry does not publish a build" — never "average build".
'
shoeWidth:
type: string
example: D
description: 'Shoe width as Texas DPS records it — a US last code (`D`, `EEE`, `B`), not a measurement. **Texas only**, same empty-string caveat and same `UNKNOWN` normalisation as `shoeSize`. NOTE: `""` on 100% of records until the next Texas bulk drop — the reader landed 2026-08-04 and no export has been ingested since.
'
Record:
type: object
description: 'One normalized offender record (offenders.io-parity superset + our extensions).
**Every key is always present — we never omit a key.** Read the empty values carefully, because they mean different things:
* `""` on a string field means **the source registry does not publish that field for this record**. Registries differ enormously (Maine publishes no sex/race/height at all). This is a property of the source, not a transient gap on our side.
* `null` on `dob` means the registry publishes no date of birth — check `age`, since many portals publish age only.
* `null` on `flags.absconder` / `flags.predator` means **unknown / not published**. It is NOT the same as `false`, which means the registry affirmatively says the person is not an absconder/predator.
* `null` on `matchedName` means your query supplied no name; `null` on `matchState` means your query supplied no `dob` or `age`.
* `null` on `stateData` (together with `offenses: []` and `images: []`) means **you did not request deep detail** — the engine strips all three unless the request carries `include: ["stateData"]`. This is the one empty value that says nothing about source coverage: re-request with `include: ["stateData"]` before concluding data is absent.
'
properties:
recordId:
type: string
description: 'Our per-record id, `rec_` + 20 hex. Hashed from JURISDICTION + the registry uuid (or, when the registry publishes no uuid, jurisdiction + record url + full name + dob).
*** NOT A STABLE KEY FOR A PERSON. *** recordId is derived from the MERGE: when we find one person in several registries we merge those rows and one WINS, and recordId (like uuid) describes the winner. Which row wins depends on which registries were in YOUR search, so narrowing the scope changes the id for the same human with nothing re-ingested. Verified on production 2026-08-05, one man (Stephen Vincent Allen, dob 1961-09-25, TX Sid=07428710 and SC Id=583650): nationwide -> rec_6d066763131ebe8c77ea (uuid 07428710); jurisdictions:["SC"] -> rec_b0b4abc2433b71400670 (uuid 583650); jurisdictions:["NSOPW"] -> rec_f8c63c0ec62e7563bd6c. One person, four ids, no defect.
CONSEQUENCE: collecting ids from state-scoped queries and then checking them against a nationwide result reports a correct merge as a MISSING RECORD. Compare results as PEOPLE (name + dob + registrationState, or the `sources[]` entries), never as id sets across different scopes.
DETERMINISTIC WITHIN ONE SCOPE: re-run the same request and you get the same ids. Store the scope with the id.
BREAKING 2026-08-04: the jurisdiction was added, so every recordId for a record carrying a uuid changed once. It previously hashed the uuid ALONE, which collapsed registrants that different registries had given the same id onto ONE recordId - source id 20059 returned rec_a82c17793c3bd390eef1 for both a Pennsylvania and a Wisconsin registrant. 147,624 records were affected. Stored ids from before that date will not match; re-key.
'
example: rec_ab12cd34ef56gh78ij90
uuid:
type: string
description: 'The REGISTRY''s own id, verbatim; "" (18%) when that registry publishes none. Unique only WITH its jurisdiction — 55,893 ids are shared across jurisdictions (147,624 records), so `20059` is a Florida, a Pennsylvania AND a Wisconsin registrant. Registries also RE-ISSUE ids in bulk, which retires the old value: treat a stored `uuid` as a cache to refresh, not as a primary key.
It is the PRIMARY registry''s id for the assembled record, so like recordId it changes when the registries covered by your search change. It is not the more stable of the two - see recordId above for which identifier to store.
'
example: IL:X24A0926
recordType:
type: string
enum:
- sex_offender
default: sex_offender
matchConfidence:
type: number
nullable: true
description: 'OUR relevance score for this match (offenders.io has none). Null when unscored. CAPPED by how the name matched, so a widened match is never reported as certainty: exact on the registered name = no cap; nickname <= 0.75; prefix / fuzzy / middle name / no first name published <= 0.60; alias-only <= 0.55. The cap is lifted only when you supplied a `dob` and the record''s full DOB matched it. A partial-name hit is a CANDIDATE match, never identity verification.
'
matchBasis:
type: array
items:
type: string
description: 'Why the record is in the result set, PER FIELD, so a widened search is never silent. Name tokens are `<field>:<how>`: `lastName:exact` / `firstName:exact` (the field equals what you sent), `lastName:prefix` / `firstName:prefix` (the field STARTS WITH what you sent), `firstName:nickname` (a nickname equivalence, e.g. bob -> Robert), `lastName:fuzzy` / `firstName:fuzzy` (spelling variant or typo), `firstName:middle` (the given name you sent is the person''s MIDDLE name, matched as a whole token of the registered legal name), `firstName:absent` (the record publishes no first name, so it could not be disproved — kept and flagged), and `alias:exact` / `alias:prefix` (an ALIAS matched, not the registered name). Also carries the legacy `lastName`/ `firstName`/`dob` tokens for which identity fields the record itself provides, plus `name_match`, `alias_match`, `middle_name_match`, or `unverified_no_dob_or_age`. Example: `["lastName:prefix","firstName:nickname","name_match"]`.
**Parsing note — this array mixes two vocabularies.** The bare `lastName` / `firstName` / `dob` tokens describe which identity fields the RECORD populates; they do NOT mean that field matched your query and they appear even when you did not search on it (a record can carry the `dob` token on a search with no `dob`). To read match quality reliably, use only the tokens containing a colon, plus `name_match`, `alias_match` and `unverified_no_dob_or_age`.
**Prefer `matchDetail`**, which is this same information already split into its separate vocabularies.
'
matchDetail:
type: object
description: 'The UNMIXED reading of `matchBasis` — the same information, already split into the three separate things that array encodes, so you never have to know which token shape means what. Always present. `matchBasis` is left byte-for-byte unchanged for existing integrations; NEW CODE SHOULD READ THIS FIELD.
Caution on `strategies.lastName: "exact"` — it means the queried surname equalled the record''s surname OR one TOKEN of a compound/hyphenated surname ("Hamilton-Smith" matches a "Smith" query, uncapped). It is not a guarantee of string equality; whole-surname matches simply sort first.
'
properties:
strategies:
type: object
additionalProperties:
type: string
enum:
- exact
- nickname
- initial
- prefix
- fuzzy
- middle
- absent
description: "HOW each field matched, as `{field: strategy}`. The three possible keys are `lastName`, `firstName` and `alias`; `alias` is present ONLY when an alias rather than the registered name produced the hit.\n\nThe strategy vocabulary is CLOSED — these seven values are all that can appear, and an unrecognised engine token would surface under `other` instead. Listed strongest first, with the ceiling each imposes on `matchConfidence` and the fields it can appear on:\n\n* `exact` (rank 1, no cap) — the field equals your value, or equals one\n token of a compound surname. On `lastName`, `firstName`, `alias`. On\n in every `match` mode and cannot be disabled.\n\n* `nickname` (rank 2, <= 0.75) — a known given-name equivalence\n (bob -> Robert). **`firstName` only**: there is no surname nickname\n table, and requesting one on `lastName` is a 422.\n\n* `initial` (rank 2, <= 0.60) — a single letter against a full given\n name (J -> John), either direction. `firstName` only.\n\n* `prefix` (rank 3, <= 0.60) — the field STARTS WITH your value,\n minimum 3 characters. On `lastName`, `firstName`, `alias`.\n\n* `fuzzy` (rank 4, <= 0.60) — spelling variant
# --- truncated at 32 KB (65 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/offendersearch-api/refs/heads/main/openapi/offendersearch-api-records-api-openapi.yml