Evidence and re-verification
Every live check is captured and hash-pinned; every finished search can be run again. Here is how to read the one and trigger the other.
What you’ll build
The audit side of an order: list the official-source captures taken for it, fetch one capture’s stored bytes and check them against the published hash, and understand what the evidence record pins. Then the re-verification path: POST /v1/fcra/searches/{search_id}/reverify adds a fresh search of the same type, marks the old one superseded, and returns the order to in_progress — with the consequences for the report, PDF and receipt.
Before you start
- A completed order. Note: sandbox searches make no live calls and capture nothing, so
GET /v1/fcra/orders/{order_id}/evidencereturns an emptycaptures[]on the sandbox; the shapes below are what a production order returns. - Capture kinds:
source_response(what the official source returned),parsed_result(what was read from it),service_result. Each carriessha256andsizeof the full body; bytes are stored up to a limit andtruncatedsays whether the stored copy is cut. - The evidence record (written once per accepted order, and for a purpose refusal) holds the end user, purpose, consent attestation, subject identifiers, what each source answered, the control version and hash, and every record furnished or withheld with its reason. It is never edited.
Step by step
- List the captures.
GET /v1/fcra/orders/{order_id}/evidence→{orderId, captures[]}. This list includes captures of superseded searches; the report’sevidence[]lists only current ones. - Fetch one capture.
GET /v1/fcra/orders/{order_id}/evidence/{capture_id}returns the stored bytes with the capture’s own content type andX-Capture-SHA256,X-Capture-Kind,X-Capture-Source,X-Capture-Truncated. - Check the hash. SHA-256 the bytes you received; when
X-Capture-Truncatedisfalseit equalsX-Capture-SHA256and the capture’ssha256. - Re-verify when you need a fresh answer.
POST /v1/fcra/searches/{search_id}/reverify→202with the new search atqueued. Wait for itssearch.completedand the order’s neworder.completed.
curl https://api.offendersearch.app/v1/fcra/orders/fo_ee0b34c5b52fd35b/evidence \
-H "X-API-Key: $OFFENDERSEARCH_KEY"{
"orderId": "fo_ee0b34c5b52fd35b",
"captures": [
{ "captureId": "fc_31f0a9c2d4e6b801", "searchId": "fs_452884eecde9a1f2", "source": "WI",
"kind": "source_response", "url": null,
"retrievedAt": "2026-10-02T14:01:31.004812+00:00", "httpStatus": null,
"contentType": "text/html; charset=utf-8",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "size": 48213, "truncated": false },
{ "captureId": "fc_31f0b2e5a7c9d013", "searchId": "fs_452884eecde9a1f2", "source": "WI",
"kind": "parsed_result", "url": null,
"retrievedAt": "2026-10-02T14:01:31.117320+00:00", "httpStatus": null,
"contentType": "application/json",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "size": 2210, "truncated": false }
]
}import hashlib, os, requests
BASE = "https://api.offendersearch.app"
H = {"X-API-Key": os.environ["OFFENDERSEARCH_KEY"]}
def fetch_capture(order_id, capture_id):
res = requests.get(BASE + "/v1/fcra/orders/" + order_id + "/evidence/" + capture_id,
headers=H, timeout=60)
res.raise_for_status()
digest = hashlib.sha256(res.content).hexdigest()
intact = (res.headers["X-Capture-Truncated"] == "false"
and digest == res.headers["X-Capture-SHA256"])
return {
"kind": res.headers["X-Capture-Kind"],
"source": res.headers["X-Capture-Source"],
"contentType": res.headers.get("Content-Type"),
"bytes": res.content,
"hashMatches": intact,
}
def reverify(search_id):
res = requests.post(BASE + "/v1/fcra/searches/" + search_id + "/reverify", headers=H, timeout=30)
if res.status_code == 409:
raise RuntimeError(res.json()["detail"]["code"]) # search_not_finished | order_cancelled
res.raise_for_status() # 202
return res.json()["id"] # the NEW fs_ id, status queued{
"id": "fs_b71e4d2c9a05f368",
"type": "criminal",
"status": "queued",
"result": null,
"recordCount": 0,
"withheld": {},
"createdAt": "2026-10-25T16:40:12.771004+00:00",
"startedAt": null,
"completedAt": null,
"supersededBy": null,
"coverage": [],
"incompleteReasons": []
}Afterwards GET /v1/fcra/orders/{order_id} shows the old search with status: "superseded" and supersededBy: "fs_b71e4d2c9a05f368", and the order back at in_progress with completedAt: null. When the new search finishes, the order completes again — with a new reportSha256, a newly rendered PDF, a new pdfSha256 and a new receipt.
Reading the result
| Field | Values | What it decides |
|---|---|---|
captures[].kind | source_response, parsed_result, service_result | What the bytes are: the source’s answer, what was read from it, or the service result. |
captures[].sha256, size | hex, bytes | Of the full body as retrieved. Compare only when truncated is false. |
captures[].truncated | boolean | The stored copy was cut at the storage limit; the hash still describes the full body. |
coverage[].captureIds[] (on the search) | fc_… | Which captures back which coverage row — the join between result and evidence. |
reverify 202 → status | queued | A new fs_ id. The old search becomes superseded; the order becomes in_progress. |
reverify 409 | search_not_finished, order_cancelled | Only a complete, unavailable or error search on a live order can be re-run. |
Edge cases and pitfalls
- Empty `captures[]` on the sandbox is correct, not a bug; test your hash check against a production order.
- The capture’s media type varies (
text/html,application/json, …); readContent-Type, do not assumeapplication/octet-stream. - A re-verify emits no event of its own. You learn it ran from the later
search.completed(newsearchId) andorder.completed. Record the new id when you call reverify. - Hashes change after a re-verify completes. Anyone holding the previous PDF will find its
pdfSha256no longer matches the current receipt; keep the old file and receipt together if you need them. - A re-run is a new search on your account of the same type and the same
exclusionssetting; it does not create a new order. - The evidence list keeps superseded searches’ captures; the report’s
evidence[]does not. Use the list for a full audit trail.
Production checklist
- Store
captureIds with each adjudicated order; fetch bytes on demand, verify the hash. - Record the new
fs_id returned by every reverify; expect a new receipt afterwards. - Do not state a retention period to end users; none is published.
- Keep the report PDF and its receipt together at the time of adjudication.