Developer docs · v1.0.0

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

Envelope of the Exact Match response (records and sourceStatus folded)
{
  "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 */ ]
}
FieldIn this responseWhat 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.
elapsedMs236Wall-clock time the search took, in milliseconds. Informational.
counts.records1People found across each registry in scope, before paging.
counts.sourcesQueried58Registries in scope for this search. With no jurisdictions filter that is the 58 US registries.
counts.sourcesComplete / sourcesIncomplete58 / 0How many finished and how many did not. sourcesIncomplete > 0 is the same information as status: "partial", as a number.
counts.sourcesSkippedByScope0Registries left out because the request scoped them out (jurisdictions, locationScoped).
counts.recordsReturned1Records on this page. Compare with counts.records to know whether to page.
capped, cappedReason, cappedLimit, cappedOmittedSourcesfalse, 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, totalPages1, 1, 1Where 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:

The OH row of sourceStatus
{
  "source": "OH",
  "status": "complete",
  "matched": 1,
  "fromCache": true,
  "note": null,
  "incomplete": false,
  "incompleteReason": null,
  "cappedOmitted": false
}
FieldWhat it means
sourceThe registry code, a US state or territory abbreviation. The same code appears in record.source.jurisdiction and record.registrationState.
statusWhether the registry answered. Describes the registry, not the search.
matchedHow many of the returned records came from this registry. A lower bound when incomplete is true.
fromCacheWhether this registry was answered from the continuously updated dataset (true) or re-verified live for this request.
incompleteTrue when this registry was not searched to the end. This is the field to branch on, independent of status.
incompleteReasonWhy, as a closed enum: deadline, truncated, excluded, not_searched, unavailable, error. Non-null exactly when incomplete is true.
noteFree text, reworded without notice. Never branch on it.
cappedOmittedTrue 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.

Identity keys and match block (person fields folded)
"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
FieldIn this responseWhat 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.
matchConfidence0.9A 0–1 score of how well the record fits the query. A ranking signal, not an identification.
matchBasissix stringsWhich query fields contributed and how: lastName:exact, firstName:exact, dob, name_match; alias:exact when the hit came through an alias.
matchDetail.strategieslastName: exact, firstName: exactThe comparison used per name field.
matchDetail.fieldsPresentlastName, firstName, dobThe 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.
unverifiedfalseTrue 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

Person and address fields
"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"
]
FieldWhat it means
name.first / middle / last / suffix / fullThe 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".
dobThe published date of birth, YYYY-MM-DD, or null when the registry publishes none.
birthYearThe year alone, present when the registry publishes a year or a full date.
dobPrecisionHow much birth evidence the registry publishes: exact here; year for year-only registries; unknown for the three Name Only sandbox records.
ageThe published age, as a string. The sandbox fixture carries "46".
sex, race, ethnicityAs published. Strings, "" when absent.
height, weight, eyeColor, hairColor, marksPhysical 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.
registrationStateThe 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 and registry detail
"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"
FieldWhat it means
offenseThe 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.
stateDataFields 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.predatorTwo 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.
housingStatushoused 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

The citation
"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… }
]
FieldIn this responseWhat 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 entryEach 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:

Criminal envelope (CR_FELONY_RESPONSE, record folded)
{
  "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 */ ]
}
DifferenceRegistry responseCriminal response
Sources countedcounts.sourcesQueried is the registries in scopecounts.sourcesOnFile and counts.sourcesPending describe the dataset; sourcesQueried counts live sources only, so a cached search shows 0.
Caps and exclusionscapped and cappedReasoncounts.candidatesCapped and counts.exclusionsIncluded (true only with include: ["exclusions"]).
legal.noticeabsentAlways present: *"Not a consumer report…"*. A hiring, tenant, credit or insurance decision goes through the FCRA Partner API instead.
sandboxabsenttrue on the sandbox host, so a misrouted call is visible in the body.
sourceStatusone row per registryEmpty 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:

Criminal record (folded to the differing keys)
"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.

  1. Read status first. A status of complete means each registry in scope finished. partial means at least one did not, and counts.sourcesIncomplete tells you how many. Never treat an empty records array as a clearance until status is complete.
  2. Read the counts. counts.records is the number of people found; counts.recordsReturned is how many are on this page. If they differ, there is a next page (page, perPage, totalPages). capped: true means the search was cut short and cappedOmittedSources lists what was left out.
  3. Scan sourceStatus. One row per registry. Any row with incomplete: true is a lower bound and incompleteReason says why. matched tells you where the hits came from before you open a single record.
  4. Open each record by identity. Key your own storage on recordId (this listing) and personRef (the person across listings). recordType tells you which schema the rest of the record follows.
  5. Decide on matchState. dob_match is the only automatic identification. year_match, age_match and no_dob_age_year route to review; null means you sent nothing to verify against. unverified: true is the one boolean that summarises all of this.
  6. Keep the citation. Store source.recordUrl, source.lastCheckedAt and source.sourceUpdatedAt, and every entry in sources[] 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.
The two fields that decide. 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, match modes, jurisdictions, tolerances and the live block are in the search reference and the background check API tutorial.
  • The async 202 and 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.