# Understanding search results

> Read the envelope before the records. The order matters.

- **HTML:** https://offendersearch.app/docs/guides/sex-offender/understanding-results
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Docs index for agents:** https://offendersearch.app/llms.txt

## Read in this order

1. **`status`.** `complete`: every source in scope finished. `partial`: at least one did not; `records[]` is a lower bound. (`pending`/`running` appear only while polling an async job; `error` means the job failed and nothing was billed for it.)
2. **`counts`.** `records` matched in total, `recordsReturned` on this page, `sourcesQueried`, `sourcesComplete`, `sourcesIncomplete`, `sourcesSkippedByScope`, and `clearedSuppressed` when clearances applied.
3. **`sourceStatus[]`.** One row per source: `source` (jurisdiction code), `status` (`ok`, `error`, `restricted`, `no_coverage` on a cached row; `complete` or `partial` on a live re-check row), `matched`, `fromCache`, `note`, `incomplete`, `incompleteReason` (`deadline`, `truncated`, `excluded`, `not_searched`, `unavailable`, `error`) and `cappedOmitted`. `status: "ok"` says the registry answered, not that it finished — read `incomplete`.
4. **`records[]`.** Best first. Each is a candidate with the match explained.
5. **`warnings[]`, `capped`.** Plain sentences for anything that shortened the answer; `capped: true` with `cappedReason: "responseLimit"` when the 4,000-record cap applied. Both empty on a normal search.

## How a record matched

| Field | Values | Use it to |
| --- | --- | --- |
| `matchConfidence` | 0–1, `null` when the search named nobody (radius, free text) | Rank and threshold. A prefix match is capped below an exact one. |
| `matchBasis[]` | e.g. `lastName:exact`, `firstName:nickname`, `alias:prefix`; `dob` appears as a bare token when the record carries one (how it agreed is `matchState`) | Explain the match to a reviewer. Prefer `matchDetail.strategies` for a per-field answer. |
| `matchState` | `dob_match`, `year_match`, `age_match`, `no_dob_age_year`, `age_mismatch`, `dob_mismatch` | Tier the review queue by how the birth date agreed. Only present when you sent `dob` or `age`. |
| `matchedName` | `{ value, type: "legal" \| "alias" }` | Show which name matched. |
| `dobPrecision` | `exact`, `year`, `year_month`, `unknown` | Know how much birth date the source published; never compare a `year` record as a full date. |
| `unverified` | boolean | The record could not be checked against the identifiers you sent (no published DOB or age). |

`matchDetail.strategies` carries one key per name field you sent (`lastName`, `firstName`) with how it matched (`exact`, `nickname`, `initial`, `prefix`, `fuzzy`, `middle`, `absent`); the birth date is not a strategy — its agreement is `matchState` (also echoed as `matchDetail.verification`). Gate an automatic decision on `strategies` and `matchState`, not on `matchConfidence` alone.

When `records[]` is non-empty **and** `counts.sourcesIncomplete > 0`, treat the result as both: the candidates are real and must be reviewed, and the answer is a lower bound, so carry the incomplete sources with the outcome (or re-run with `live` on those registries) rather than reporting `clear` or dropping the candidates.

## What a record carries

`name` (`first`, `middle`, `last`, `suffix`, `full`), `aliases[]`, `nicknames[]`, `dob`, `birthYear`, `age`, physical descriptors, `addresses[]`, `registrationState`, `addressStates[]`, `offenses[]` (`crime`, `statute`, `convictionDate`, `offenseDate`, `registrationDate`, …), `flags` (`absconder`, `predator`), `housingStatus`, `images[]`, and `sources[]` with `jurisdiction`, `registryName`, `recordUrl` and `lastCheckedAt`. `stateData` is present on every record (`exclude: ["stateData"]` drops it for a lighter payload). Every field: [the Record object](/docs/record-object).

## The decision each answer supports

| Answer | Means | Does not mean |
| --- | --- | --- |
| `complete`, `records: []` | No registrant matched in any source searched. | That the person is not registered anywhere; coverage is what the catalog says it is. |
| `partial`, `records: []` | No match in the sources that finished. | A clear result. Re-check the incomplete sources. |
| Records with `dob_match` | A registrant with this name whose published birth date equals the one you sent. | Confirmed identity. Names and birth dates repeat. |
| Records with `year_match` / `age_match` / `no_dob_age_year` | Name agreed; birth date partly or not at all. | A match to act on without review. |

## Related

- [Completeness & source status](https://offendersearch.app/docs/result-completeness)
- [Matching & confidence](https://offendersearch.app/docs/matching)
- [Searching by date of birth](https://offendersearch.app/docs/date-of-birth)
