Offendersearch
Criminal API Reference

Result completeness & source status

On a live search, an incomplete answer is labelled - counts, sourceStatus and incompleteReason.

Base URL https://api.offendersearch.app

The incompleteness signal is a property of live verification

A cached search always returns a whole answer from the corpus in one query, so sourceStatus[] is [], status is "complete", and an empty records genuinely means “nobody in the corpus matched”. The “is it whole?” question only arises for live sources, where we reach out in real time.

sourceStatus[] — the live-verification audit trail

On a cached search this is []. On a live search, one entry per live source.

ok says the source responded; incomplete says whether the search finished. A source can be ok: true and incomplete: true at the same time — read incomplete, never ok, to decide whether a 0 is no match or unknown. A failed live check is reported, not hidden, and not billed.
{ "code": "TX-HARRIS", "source": "TX-HARRIS", "ok": false, "incomplete": true,
  "incompleteReason": "captcha_unsolved", "records": 0,
  "lastCheckedAt": "2026-08-25T15:04:12+00:00", "live": true }

incompleteReason — the closed vocabulary

incompleteReason is non-null only when incomplete is true. The transport reasons are the retryable ones; truncated_over_cap is stable (narrow instead); the rest are not retryable automatically. In every case, an incomplete source’s 0 means unknown, not no match.

incompleteReasonWhat you should do
timeoutRetry — a capacity event that clears.
source_downRetry, with backoff.
rate_limitedRetry later.
captcha_unsolvedRetry — transient; do not record absence.
proxy_requiredRetry.
truncated_over_capDo not retry — narrow. Add a first name or a date of birth.
paid_gatewayNot retryable automatically. Record not determined.
login_requiredNot retryable automatically. Record not determined.
unsupported_siteA live.websites entry mapping to no covered jurisdiction. Resolve it or drop it.
not_searchedA requested code that resolved to no live jurisdiction. Confirm it against the catalog.
const RETRYABLE = new Set(["timeout", "source_down", "rate_limited",
                           "captcha_unsolved", "proxy_required"]);
const shortAndRetryable = res.sourceStatus.filter(
  s => s.incomplete && RETRYABLE.has(s.incompleteReason)
);
if (shortAndRetryable.length) {
  // Hold. Do NOT record "not found" — re-run the live check.
}

Reading an empty live result

Two very different events produce the same empty list on a live search, and the response tells you which one you are holding.

What happenedWhat the response says
Every source completed and nobody matched. This is an answer.status: "complete", counts.sourcesIncomplete: 0
One or more sources could not be checked to the end. This is a lower bound.status: "partial", counts.sourcesIncomplete > 0, and a warnings[] sentence naming the sources

Never infer absence from an empty live result without checking counts.sourcesIncomplete == 0 first.

if (res.counts.sourcesIncomplete > 0) {
  // Record "not determined", not "not found". Re-run the live check.
}

A worked mixed live response

A live search across two sources where one completed and one was blocked. Read it in order: status is "partial"; counts.sourcesIncomplete is 1 (TX-HARRIS was blocked, so its 0 means unknown); warnings[0] names it and its reason, captcha_unsolved, is retryable. The blocked source is not billed.

200 OK — partial
{
  "status": "partial",
  "counts": { "records": 3, "recordsReturned": 3,
              "sourcesQueried": 2, "sourcesComplete": 1, "sourcesIncomplete": 1 },
  "warnings": [
    "INCOMPLETE SEARCH: 1 of 2 sources could not be searched to completion (TX-HARRIS). The records below are a LOWER BOUND: an absent person may simply not have been reached. This is NOT evidence that someone is absent — retry, or narrow the query."
  ],
  "sourceStatus": [
    { "code": "TX-DALLAS", "source": "TX-DALLAS", "ok": true,  "incomplete": false,
      "incompleteReason": null, "records": 3, "live": true },
    { "code": "TX-HARRIS", "source": "TX-HARRIS", "ok": false, "incomplete": true,
      "incompleteReason": "captcha_unsolved", "records": 0, "live": true }
  ],
  "records": [ "… 3 records, a mixture of matchStates …" ],
  "usage": { "liveChecks": 1, "liveChargeUsd": 0.02, "capped": false },
  "searchId": "crs_9a3b71c0e28d4f6a5b12",
  "legal": { "notice": "Not a consumer report. …" }
}

counts

  • records — total matched, before the page slice. Not records.length.
  • recordsReturned — how many records this response carries — the current page.
  • sourcesQueriedlive sources queried this request. 0 on a cached search.
  • sourcesComplete — of those, how many finished. status: "complete" means exactly sourcesComplete == sourcesQueried.
  • sourcesIncompletethe one count that means something may be missing. > 0 → your result is a lower bound. 0 on a cached search.

sourcesQueried: 0 next to sourcesIncomplete: 0 on a cached search means “complete”, not “nothing was searched”. See Pagination for how counts.records drives the pager.