# Verification reports

> A timestamped PDF of a search you already ran, with a source citation on every record. This is the document that goes in the file.

- **HTML:** https://offendersearch.app/docs/guides/sex-offender/verification-reports
- **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

## When to use this

When a search decided something — a visitor was admitted, a volunteer was cleared, a candidate was escalated — the file needs more than a `searchId` in a database. `POST /v1/report` renders one consolidated PDF for a search on your account: who was viewing it, what was asked, each registrant that matched, with every field on file, a source citation on each record, and the legal notice. You will build the step that requests the PDF from the `searchId` you stored, saves it with its `X-Report-Id`, and does so inside the window that makes it possible.

## Before you start

- A test key, or a dashboard session — this route accepts either. The sandbox renders the production PDF for the sample people, without photographs, and bills nothing.
- A `searchId` from a prior `POST /v1/search` or `POST /v1/searches` on the **same account**, created within the last **7 days**. Batch rows have no `searchId` and cannot be reported on.
- Price: one PDF charge (**$0.02**) on top of the search that was already billed. The legacy one-shot mode — an inline `query` instead of `searchId` — runs a new search and bills a call as well ([pricing](/pricing)).

> **The one thing people get wrong.** The window is **7 days**, shorter than the 14-day retention of stored results. A report request for an older search answers `410`. Request the PDF when the decision is made, not when the audit arrives.

## Step by step

1. **Run the search and keep the `searchId`.** Search **Exact Match** with `dob` `1980-01-01`. Store `searchId`.
2. **Request the report.** `POST /v1/report` with the `searchId` and the optional viewer, purpose and reference fields. The body of the `200` is the PDF.
3. **Save it with its id.** Read `X-Report-Id` (`rpt_` + 12 hex) and `Content-Disposition` from the headers; store the id next to the `searchId`.
4. **Handle the refusals.** `404` for a `searchId` this account never ran; `410` past 7 days; `400` when neither `searchId` nor `query` was sent.

### Step 2 — the request

**cURL**
```bash
curl -s -D headers.txt -o report.pdf https://sandbox.offendersearch.app/v1/report \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" -H "Content-Type: application/json" \
  -d '{"searchId": "srch_7a89158926d834f43f66856f",
       "viewerName": "Admissions desk", "viewerEmail": "desk@example.com",
       "purpose": "Visitor screening", "reference": "visit-2026-10-25-0417"}'
grep -i "x-report-id\|content-disposition" headers.txt
```

**Python**
```python
import os, requests

BASE = os.environ.get("OFFENDERSEARCH_BASE", "https://sandbox.offendersearch.app")
H = {"X-API-Key": os.environ["OFFENDERSEARCH_KEY"]}

def report(search_id, viewer_name=None, viewer_email=None, purpose=None, reference=None):
    body = {"searchId": search_id, "viewerName": viewer_name, "viewerEmail": viewer_email,
            "purpose": purpose, "reference": reference}
    r = requests.post(BASE + "/v1/report", headers=H, json={k: v for k, v in body.items() if v}, timeout=120)
    if r.status_code == 410:
        raise LookupError("older than the 7-day report window; run a new search")
    if r.status_code == 404:
        raise LookupError("no such search on this account")
    r.raise_for_status()
    assert r.headers["Content-Type"].startswith("application/pdf")
    return r.headers["X-Report-Id"], r.content

report_id, pdf = report("srch_7a89158926d834f43f66856f", viewer_name="Admissions desk",
                        purpose="Visitor screening", reference="visit-2026-10-25-0417")
with open(f"{report_id}.pdf", "wb") as f:
    f.write(pdf)
print(report_id, len(pdf), "bytes")
```

**JavaScript**
```javascript
import { writeFile } from 'node:fs/promises';

const BASE = process.env.OFFENDERSEARCH_BASE ?? 'https://sandbox.offendersearch.app';
const H = { 'X-API-Key': process.env.OFFENDERSEARCH_KEY, 'Content-Type': 'application/json' };

async function report(searchId, meta = {}) {
  const res = await fetch(BASE + '/v1/report', { method: 'POST', headers: H, body: JSON.stringify({ searchId, ...meta }) });
  if (res.status === 410) throw new Error('older than the 7-day report window; run a new search');
  if (res.status === 404) throw new Error('no such search on this account');
  if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + (await res.text()));
  const reportId = res.headers.get('X-Report-Id');
  return { reportId, pdf: Buffer.from(await res.arrayBuffer()) };
}

const { reportId, pdf } = await report('srch_7a89158926d834f43f66856f', {
  viewerName: 'Admissions desk', purpose: 'Visitor screening', reference: 'visit-2026-10-25-0417',
});
await writeFile(reportId + '.pdf', pdf);
console.log(reportId, pdf.length, 'bytes');
```

*200 — response headers*
```http
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="offendersearch-verification-rpt_97a079557016.pdf"
X-Report-Id: rpt_97a079557016
```

The body is the PDF, not JSON. `viewerName`, `viewerEmail`, `purpose` and `reference` are optional and are printed at the top of the document as a record of who ran it and why; they are not an access gate. `requesterName` is a legacy alias of `viewerName`.

### What the PDF contains

- The report id and the time it was generated; the viewer identity, purpose and reference you sent.
- The query as it was run and the registries searched.
- Every matching registrant with every field on file: names and aliases, date of birth or age and how it matched, addresses and jurisdiction, offenses and risk designation, photograph where available, and the extended per-state fields.
- A source-attribution section with a citation on every record — the registry and the page it came from.
- A legal notice: informational use only; not a consumer report; the use restrictions.

### Step 4 — the refusals

**404**
```json
{ "detail": "No search found for that searchId on this account." }
```

**410**
```json
{ "detail": "This search is older than the 7-day verification-report window. Run a new search to report on it." }
```

**400**
```json
{ "detail": "Provide a searchId (the id of a prior search) or an inline query." }
```

## Reading the result

| Where | Field | Keep it for |
| --- | --- | --- |
| Header | `X-Report-Id` (`rpt_` + 12 hex) | The document's identity; printed on the PDF. Store with the `searchId`. |
| Header | `Content-Disposition` | The suggested filename, `offendersearch-verification-{reportId}.pdf`. |
| Request | `searchId` | Which search the report cites. It must be on this account and under 7 days old. |
| Request | `viewerName`, `viewerEmail`, `purpose`, `reference` | Printed on the first page: who, why, and your case id. |
| Request (legacy) | `query`, `jurisdictions`, `locationScoped` | One-shot mode: runs and bills a new search. Prefer `searchId`. |

## Edge cases and pitfalls

- **The report re-runs a reduced form of the query** at report time — `firstName`, `lastName`, `dob`, `age`, `city`, `state`, `zipcode`, `jurisdictions`, `locationScoped` — against the data at that moment. A search that used `county`, a radius, `q`, `nameMatch`/`prefixMatch`, a non-default `match`, a `live` block or a clearance scope can produce a PDF that differs from the envelope you read. Request the report promptly, and for a live decision also keep the [evidence documents](/docs/guides/sex-offender/evidence-documents).
- **Batch rows cannot be reported.** Re-run the person with `POST /v1/search` to obtain a `searchId`.
- **7 days, not 14.** `/results` keeps records for 14 days; `/report` accepts a `searchId` for 7.
- **It is one PDF per search**, not per registrant; a search with three candidates yields one document listing all three.
- **Trial accounts** can call `/report`; the route has no paid-entitlement gate and the PDF is metered at the PDF rate rather than as a trial unit, so expect it on the first invoice.
- **Proof documents are a different thing.** `POST /v1/searches/{searchId}/proof` renders per-registry look-alike documents for named registries and requires billing; it is an internal add-on, not the verification report. See [Verification reports](/docs/reports) for the distinction.
- **Not FCRA.** The notice printed on the document says so: this report is not a consumer report and must not be the sole basis for an employment, housing, credit or insurance decision. That path is the [FCRA Partner API](/fcra).

## Production checklist

- The report is requested in the same workflow step that records the decision, inside 7 days.
- `X-Report-Id` stored with `searchId`, `recordId`s and the reviewer's verdict.
- `viewerName`/`purpose`/`reference` filled from your own case data so the first page answers the auditor's questions.
- PDFs stored under your own retention policy; the platform keeps the search 14 days.

## Related

- [Verification reports (PDF)](https://offendersearch.app/docs/reports)
- [Evidence and supporting documents](https://offendersearch.app/docs/guides/sex-offender/evidence-documents)
- [Retrieving individual records](https://offendersearch.app/docs/guides/sex-offender/retrieving-records)
- [Platform: operations](https://offendersearch.app/docs/platform/operations)
