Background check API response explained, field by field
One real sandbox response, read group by group: what each key in the envelope, the sourceStatus rows and the record means, which fields decide identity, and how the criminal response differs.
Updated November 3, 2026
The background check API response at a glance
This page reads one real response from top to bottom. It is the answer the sandbox gives for the test person Exact Match with dob 1980-01-01: one registrant in Ohio, identified by a full date of birth, listed by one registry. The shape is exactly what the production host returns for the background check API; only the values are fixtures. The request that produces it is three lines in any language. Python and Node.js SDKs ship with the first registry release; see SDKs. Until then the raw HTTP calls below are the integration.
curl -X POST https://sandbox.offendersearch.app/v1/search \
-H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{"query": {"firstName": "Exact", "lastName": "Match", "dob": "1980-01-01"}}'A response is three layers. The envelope says what happened to the search as a whole. The sourceStatus array says what happened at each registry. The records array holds the people found, and each record is itself four groups: identity and match, the person, the offense and registry detail, and the citation. Read them in that order and you will never mistake a partial answer for a clear one or a name-only lead for an identification. The reference pages (record object, matching, result completeness) define every key; this page is the walk-through.
The response envelope: searchId, status, counts and paging
{
"searchId": "srch_sbx_so_exact",
"status": "complete",
"elapsedMs": 236,
"counts": {
"records": 1,
"sourcesQueried": 58,
"sourcesIncomplete": 0,
"sourcesComplete": 58,
"sourcesSkippedByScope": 0,
"recordsReturned": 1
},
"capped": false,
"cappedReason": null,
"cappedLimit": null,
"cappedOmittedSources": [],
"omittedFields": [],
"page": 1,
"perPage": 1,
"totalPages": 1,
"proof": {
"status": "none"
},
"warnings": [],
"sourceStatus": [ /* 58 rows */ ],
"records": [ /* 1 record */ ]
}| Field | In this response | What it means |
|---|---|---|
searchId | "srch_sbx_so_exact" | The id of this search. Persist it: a repeated POST is a second billed search, and async searches are read back by this id. |
status | "complete" | complete when each registry in scope finished; partial when at least one did not. The first field to branch on. |
elapsedMs | 236 | Wall-clock time the search took, in milliseconds. Informational. |
counts.records | 1 | People found across each registry in scope, before paging. |
counts.sourcesQueried | 58 | Registries in scope for this search. With no jurisdictions filter that is the 58 US registries. |
counts.sourcesComplete / sourcesIncomplete | 58 / 0 | How many finished and how many did not. sourcesIncomplete > 0 is the same information as status: "partial", as a number. |
counts.sourcesSkippedByScope | 0 | Registries left out because the request scoped them out (jurisdictions, locationScoped). |
counts.recordsReturned | 1 | Records on this page. Compare with counts.records to know whether to page. |
capped, cappedReason, cappedLimit, cappedOmittedSources | false, null, null, [] | Whether the search was cut short by a result cap, why, at what limit, and which registries were omitted as a result. |
omittedFields | [] | Record fields left out of this response. stateData appears here when it was not requested with include. |
page, perPage, totalPages | 1, 1, 1 | Where you are in the result set. See pagination. |
proof.status | "none" | Whether live evidence was requested and produced. none on an ordinary search. |
warnings | [] | Human-readable notes about the request. Log them; never branch on their text. |
Two of these decide everything downstream. status tells you whether the answer is a whole answer. counts.records versus counts.recordsReturned tells you whether you have seen it all. A clearance workflow that reads only records.length will call a partial search with zero hits "clear"; the fix is to read status first, always.
sourceStatus: one row per registry in the response
The sourceStatus array has one row for each registry in scope, 58 rows on an unscoped search. The Exact Match response has 57 rows that read "status": "complete", "matched": 0 and one that does not:
{
"source": "OH",
"status": "complete",
"matched": 1,
"fromCache": true,
"note": null,
"incomplete": false,
"incompleteReason": null,
"cappedOmitted": false
}| Field | What it means |
|---|---|
source | The registry code, a US state or territory abbreviation. The same code appears in record.source.jurisdiction and record.registrationState. |
status | Whether the registry answered. Describes the registry, not the search. |
matched | How many of the returned records came from this registry. A lower bound when incomplete is true. |
fromCache | Whether this registry was answered from the continuously updated dataset (true) or re-verified live for this request. |
incomplete | True when this registry was not searched to the end. This is the field to branch on, independent of status. |
incompleteReason | Why, as a closed enum: deadline, truncated, excluded, not_searched, unavailable, error. Non-null exactly when incomplete is true. |
note | Free text, reworded without notice. Never branch on it. |
cappedOmitted | True when the result cap dropped this registry's hits. |
The row is how you turn "zero records" into a sentence you can defend: *searched 58 registries, all complete, none matched* is a clearance; *searched 58, two incomplete because of a deadline* is not, and the incomplete results guide shows how to retry just those two.
Identity keys and the match block of a response record
The top of every record says who this is to the API, and why it was returned for your query. Four identity keys come first, then the match block.
"recordId": "rec_28db9639d712c4c61e8e",
"personRef": "prs_28db9639d712c4c61e8e",
"uuid": "SBX-OH-100001",
"recordType": "sex_offender",
"matchConfidence": 0.9,
"matchBasis": [
"lastName",
"firstName",
"dob",
"lastName:exact",
"firstName:exact",
"name_match"
],
"matchDetail": {
"strategies": {
"lastName": "exact",
"firstName": "exact"
},
"fieldsPresent": [
"lastName",
"firstName",
"dob"
],
"signals": [
"matchedLegalName"
],
"matchedNameType": "legal",
"verification": "dob_match"
},
"matchedName": {
"value": "Exact Sample Match",
"type": "legal"
},
…
"dobVerification": "dob_match",
"matchState": "dob_match",
"unverified": false| Field | In this response | What it means |
|---|---|---|
recordId | "rec_28db9639d712c4c61e8e" | This listing. GET /v1/records/{recordId} fetches it again. |
personRef | "prs_28db9639d712c4c61e8e" | The person, stable across listings and across searches. The key to deduplicate and to monitor on. |
uuid | "SBX-OH-100001" | The registry's own identifier for the listing, as published. |
recordType | "sex_offender" | Which schema the rest of the record follows. Always sex_offender on /v1/search. |
matchConfidence | 0.9 | A 0–1 score of how well the record fits the query. A ranking signal, not an identification. |
matchBasis | six strings | Which query fields contributed and how: lastName:exact, firstName:exact, dob, name_match; alias:exact when the hit came through an alias. |
matchDetail.strategies | lastName: exact, firstName: exact | The comparison used per name field. |
matchDetail.fieldsPresent | lastName, firstName, dob | The query fields that were available to match on. |
matchDetail.signals | ["matchedLegalName"] | Named reasons the record scored as it did. |
matchDetail.matchedNameType | "legal" | legal or alias: which of the person's names your query hit. |
matchDetail.verification | "dob_match" | The same value as matchState, inside the detail object. |
matchedName | { value: "Exact Sample Match", type: "legal" } | The name string that matched and its type. For Alias Match on the sandbox this is type: "alias". |
dobVerification | "dob_match" | What the date-of-birth check concluded. |
matchState | "dob_match" | The identification verdict. The one field a decision should branch on. |
unverified | false | True whenever matchState is anything other than dob_match. One boolean for the review queue. |
matchState takes six values. dob_match: the registry publishes a full date and it agrees with yours, the only automatic identification. year_match: the registry publishes only a year and it agrees. age_match: the registry publishes an age and it agrees within ageTolerance. no_dob_age_year: the registry publishes nothing to check, so the name is the only evidence. dob_near_match: only with dobTolerance, always unverified. null: you sent no dob or age, so nothing was checked. A record whose published date contradicts yours is not in the response at all; the sandbox demonstrates this when the same name is searched with 1990-05-05 and zero records come back. The matching and date of birth references, and the sibling tutorial on no-match and multiple-match results, cover the decision table.
The person block and addresses in a background check API response
"name": {
"first": "Exact",
"middle": "Sample",
"last": "Match",
"suffix": "",
"full": "Exact Sample Match"
},
"aliases": [],
"nicknames": [],
"dob": "1980-01-01",
"birthYear": 1980,
"dobPrecision": "exact",
"age": "46",
"sex": "M",
"race": "White",
"ethnicity": "Not Hispanic",
"addresses": [
{
"type": "residence",
"line1": "100 Sample St",
"city": "Sampleville",
"county": "Sample",
"state": "OH",
"zipcode": "00000",
"lat": null,
"lng": null
},
{ "type": "employment", … }
],
"registrationState": "OH",
"addressStates": [
"OH"
]| Field | What it means |
|---|---|
name.first / middle / last / suffix / full | The legal name as published. suffix is "" when there is none; full is the display string. |
aliases[], nicknames[] | Other names the registry lists. Empty arrays, never missing. A hit through one of these shows matchedName.type: "alias". |
dob | The published date of birth, YYYY-MM-DD, or null when the registry publishes none. |
birthYear | The year alone, present when the registry publishes a year or a full date. |
dobPrecision | How much birth evidence the registry publishes: exact here; year for year-only registries; unknown for the three Name Only sandbox records. |
age | The published age, as a string. The sandbox fixture carries "46". |
sex, race, ethnicity | As published. Strings, "" when absent. |
height, weight, eyeColor, hairColor, marks | Physical description as published, strings. Useful for a human reviewer, never for automated identity. |
addresses[] | Every address the registry lists, each with type (residence, employment, …), line1, city, county, state, zipcode, and lat/lng when geocoded. |
registrationState | The registry that lists this person. The sandbox person Out State is registered by OK and lives in TX. |
addressStates[] | Every state that appears in addresses[]. This is what a state query filters on, which is why Out State comes back for state: "TX". |
Two things catch new integrations here. First, dob and dobPrecision describe what the registry publishes, not what you sent; the match block describes the comparison. Second, registrationState and addressStates are different questions, *who lists this person* and *where do they live or work*, and a person can answer them differently.
Offense, stateData, flags, images and housingStatus in the response
"offense": {
"crime": "Sexual battery",
"statute": "SAMPLE 2907.03",
"tier": "Tier III",
"convictionDate": "2004-05-12",
"offenseDate": "2003-11-02",
"registrationDate": "2008-02-01",
"releaseDate": "2008-01-15",
"caseNumber": "SBX-2003-000101",
"victimAge": "Minor",
"convictionCounty": "Sample"
},
"offenses": [ { …the same object… } ],
"stateData": {
"status": "Active",
"designation": "Tier III sex offender",
"lawAgency": "Sample County Sheriff's Office",
"complianceStatus": "compliant",
"isLifetimeRegistration": true,
"lastVerificationDate": "2026-09-01",
"registrationDuration": "Lifetime"
},
"flags": {
"absconder": false,
"predator": false
},
"images": [
{
"url": "https://sandbox.offendersearch.app/v1/sandbox/photos/1.svg"
}
],
"housingStatus": "housed"| Field | What it means |
|---|---|
offense | The primary offense: crime, statute, tier, convictionDate, offenseDate, registrationDate, releaseDate, caseNumber, victimAge, convictionCounty. |
offenses[] | Every offense the registry lists, same shape. offense is always the first of these; read offenses when you need them all. |
stateData | Fields that only this registry publishes, under keys that vary by state: here status, designation, lawAgency, complianceStatus, isLifetimeRegistration, lastVerificationDate, registrationDuration. Request it with include: ["stateData"]; otherwise stateData is named in omittedFields. |
flags.absconder, flags.predator | Two booleans normalised across registries: whether the registry marks the person as non-compliant or missing, and whether it applies a predator designation. |
images[] | Photo URLs as { url }. The sandbox serves an SVG placeholder. |
housingStatus | housed here; homeless for the sandbox person Homeless Registrant, whose addresses carry no street. |
offense is the summary; offenses[] is the record. A reviewer's screen should render the array. stateData is deliberately not normalised: it is the registry's own vocabulary, and the keys under it differ from one registry to the next, so treat it as a display object rather than something to branch on.
source and sources[]: the citation on every response record
"source": {
"jurisdiction": "OH",
"registryName": "Sandbox sample registry (OH)",
"recordUrl": "https://offendersearch.app/docs/sandbox",
"personRef": "prs_28db9639d712c4c61e8e",
"recordedAt": "2025-01-10T00:00:00+00:00",
"lastCheckedAt": "2026-10-01T00:00:00+00:00",
"sourceUpdatedAt": "2026-10-01T00:00:00+00:00"
},
"sources": [
{ …the same object, one per registry that lists this person… }
]| Field | In this response | What it means |
|---|---|---|
source.jurisdiction | "OH" | The registry this listing came from. Equals registrationState. |
source.registryName | "Sandbox sample registry (OH)" | The registry's display name. |
source.recordUrl | "https://offendersearch.app/docs/sandbox" | The page a reviewer opens to confirm the listing. The citation to store with any decision. |
source.personRef | "prs_28db9639d712c4c61e8e" | The same person key, repeated per source. |
source.recordedAt | "2025-01-10T00:00:00+00:00" | When this listing first entered the dataset. |
source.lastCheckedAt | "2026-10-01T00:00:00+00:00" | When the listing was last confirmed against the registry. |
source.sourceUpdatedAt | "2026-10-01T00:00:00+00:00" | When the registry last changed the listing, where the registry publishes that. |
sources[] | one entry | Each registry that lists this person, same shape. The sandbox person Multi State carries two entries, FL and GA, merged into one record. |
source is the primary citation and sources[] is the complete list; on a single-registry person they are the same object once. Store the URL and the two timestamps with every decision you make from the record. The sex offender API page and the record object reference describe the merge rule for sources[].
How the criminal records API response differs
The criminal records API answers POST /v1/criminal/search with the same three layers, and the differences are deliberate. This is the sandbox envelope for Felony Record with dob 1975-06-15:
{
"status": "complete",
"counts": {
"records": 1,
"recordsReturned": 1,
"sourcesQueried": 0,
"sourcesComplete": 0,
"sourcesIncomplete": 0,
"candidatesCapped": false,
"exclusionsIncluded": false,
"sourcesOnFile": 1567,
"sourcesPending": 347
},
"warnings": [],
"page": 1,
"perPage": 50,
"totalPages": 1,
"sourceStatus": [],
"omittedFields": [
"sourceData"
],
"legal": {
"notice": "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.)."
},
"searchId": "srch_sbx_cr_felony",
"sandbox": true,
"records": [ /* 1 record */ ]
}| Difference | Registry response | Criminal response |
|---|---|---|
| Sources counted | counts.sourcesQueried is the registries in scope | counts.sourcesOnFile and counts.sourcesPending describe the dataset; sourcesQueried counts live sources only, so a cached search shows 0. |
| Caps and exclusions | capped and cappedReason | counts.candidatesCapped and counts.exclusionsIncluded (true only with include: ["exclusions"]). |
legal.notice | absent | Always present: *"Not a consumer report…"*. A hiring, tenant, credit or insurance decision goes through the FCRA Partner API instead. |
sandbox | absent | true on the sandbox host, so a misrouted call is visible in the body. |
sourceStatus | one row per registry | Empty on a cached search; one row per live source when a live block is sent. |
Inside the record the schema changes more. dob is an object, not a string; matchBasis and matchDetail are strings, not arrays; the offense detail lives in cases[], each case carrying charges[] with a disposition, events[] and a sentence; and sources[] entries carry a code and a level (county, state, federal) instead of a jurisdiction and registryName:
"recordType": "court_case",
"externalId": "SBX-CR-2015-000123",
"personRef": "prs_6e1aaefd188032a7e6eb",
"dob": {
"date": "1975-06-15",
"birthYear": 1975,
"birthMonth": 6,
"age": null,
"ageAsOf": null,
"dobPrecision": "full"
},
"currentStatus": "on_probation",
"matchConfidence": 0.99,
"matchState": "dob_match",
"dobVerification": "record",
"unverified": false,
"matchBasis": "lastName:exact firstName:exact dob:match",
"matchDetail": "name and full date of birth agree",
"cases": [
{
"caseNumber": "SBX-CR-2015-000123",
"court": "Sample County Court of Common Pleas",
"level": "county",
"filedDate": "2015-03-02",recordType is an open string on the criminal side: court_case, jail_booking, doc_inmate, warrant, wanted_fugitive, registry, healthcare_exclusion. A jail_booking has no disposition because a booking is not a conviction; a court_case is read through cases[].charges[].disposition. Every key is always present, with "", null or [] when the source publishes nothing, so a parser can be strict. The criminal record object and the dispositions guide finish the picture.
How to read a background check API response in order
Six reads, in this order, and the response cannot mislead you.
- Read status first. A
statusofcompletemeans each registry in scope finished.partialmeans at least one did not, andcounts.sourcesIncompletetells you how many. Never treat an emptyrecordsarray as a clearance untilstatusiscomplete. - Read the counts.
counts.recordsis the number of people found;counts.recordsReturnedis how many are on this page. If they differ, there is a next page (page,perPage,totalPages).capped: truemeans the search was cut short andcappedOmittedSourceslists what was left out. - Scan sourceStatus. One row per registry. Any row with
incomplete: trueis a lower bound andincompleteReasonsays why.matchedtells you where the hits came from before you open a single record. - Open each record by identity. Key your own storage on
recordId(this listing) andpersonRef(the person across listings).recordTypetells you which schema the rest of the record follows. - Decide on matchState.
dob_matchis the only automatic identification.year_match,age_matchandno_dob_age_yearroute to review;nullmeans you sent nothing to verify against.unverified: trueis the one boolean that summarises all of this. - Keep the citation. Store
source.recordUrl,source.lastCheckedAtandsource.sourceUpdatedAt, and every entry insources[]when a person is listed by more than one registry. A decision about a person is made from the source page, not from the summary.
status on the envelope and matchState on the record. Everything else in the response explains those two.Background check API response questions
What does a background check API response look like?
JSON with an envelope (searchId, status, counts, paging), a sourceStatus array with one row per registry, and a records array. Each record carries identity keys, a match block, the person, the offense and registry detail, and a citation. The whole Exact Match answer is reproduced on this page.
What is the difference between matchConfidence and matchState?
matchConfidence is a 0–1 ranking score for how well the record fits the query. matchState is the identification verdict from the date-of-birth check. Sort by the first; decide on the second.
Why is a record missing even though the name matches?
Because its published date of birth contradicts the one you sent. Contradicting records are dropped, not returned with a low score. On the sandbox, Exact Match with dob 1990-05-05 returns counts.records: 0.
What does status "partial" mean in the response?
At least one registry in scope did not finish. counts.sourcesIncomplete says how many, and the sourceStatus rows with incomplete: true say which and why. The records you did get are real; the registries that did not finish may hold more.
Which field is the person's stable id?
personRef. It stays the same across listings and across searches, and it is what a person monitor keys on. recordId identifies one listing and uuid is the registry's own id.
Why is dob an object in the criminal response but a string in the registry response?
Criminal sources publish birth evidence in more forms: a full date, a year, a month and year, or an age as of a date. The object carries date, birthYear, birthMonth, age, ageAsOf and dobPrecision so none of that is lost. Registries publish a date or a year, and a string with dobPrecision beside it is enough.
Is the response a consumer report?
No. The criminal response says so in legal.notice, and the registry response is subject to the same rule. A hiring, tenant, credit or insurance decision runs as an order through the FCRA Partner API; consult counsel on which applies to you.
What this page does not cover
- Building the request: fields,
matchmodes,jurisdictions, tolerances and theliveblock are in the search reference and the background check API tutorial. - The async
202and the batch row shapes, which wrap this response: see async & webhooks and batch. - The criminal record's 31 folded keys (
lifecycle,physical,identifiers,booking,warrant,supervision,parole,custody,exclusion,normalized): see the criminal record object. - FCRA order results, which carry their own result objects: see FCRA results.