# FCRA API documentation

> An orders API for approved consumer reporting agencies and screening platforms under a written FCRA agreement: one order covers a sex offender search, a criminal search or both, with controls, disputes, a consumer portal and signed webhooks.

- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed; see https://offendersearch.app/docs/fcra/sandbox.md
- **Authentication:** `X-API-Key` request header, on an account with FCRA Partner Access
- **HTML documentation:** https://offendersearch.app/docs/fcra
- **Per-section markdown:** https://offendersearch.app/docs/fcra/{section}.md

## Introduction

The FCRA API is for **approved consumer reporting agencies and screening platforms under a written FCRA agreement**. It is an orders API: one order carries a sex offender search, a criminal search, or both, for one consumer, under one end user, one permissible purpose and one consent attestation. Orders run in the background and report back by signed webhook.

- **One order, both products.** `searches: [{"type":"sex_offender"},{"type":"criminal"}]` — each search has its own status, result and records.
- **The FCRA facts travel with the order.** The end user, the permissible purpose and your consent attestation are recorded on every order, with the subject’s full date of birth.
- **Live where it matters.** Each search runs live in every state in the address history you send and in the state where the job or housing is, and in our data everywhere else. A source-by-source `coverage` table shows exactly what is behind every result, including a `clear`.
- **Controls before anything is furnished.** Exact date-of-birth matching, state restrictions, the seven-year rules for criminal records, dispute suppressions and re-verification with the official source. Anything that fails is withheld and counted by reason.
- **Disputes are an endpoint.** Open a reinvestigation on a furnished record, follow it on the 30-day clock, and receive the outcome by webhook.
- **A consumer portal on every order.** The access code on each order lets the consumer see exactly what was furnished about them and dispute any item.
- **Evidence anyone can check.** Every live check keeps hash-pinned evidence of what it returned; the stored PDF carries a QR code to a public page where anyone can confirm the file against its signed receipt without uploading it.

> Standard self-serve search is $0.15 a call, is not a consumer report and is never used for a hiring or tenant decision. Regulated checks run only through FCRA Partner Access, on this API. FCRA pricing is quoted per partner — contact us for pricing.

## What FCRA workflows are for

FCRA workflows are meant **primarily for tenant screening and hiring decisions** — any time a result will help decide whether to rent to someone or whether to hire them. A search used for those decisions runs as an FCRA order: you state the permissible purpose and attest to the consumer’s consent, and the order applies the accuracy controls and gives the consumer a dispute path. Whether a particular decision falls under the FCRA is yours to determine under your agreement.

- **Use the FCRA API** for: tenant and rental-applicant screening, pre-employment and employment screening, and any other eligibility decision your agreement covers (`permissiblePurpose`: `employment`, `tenant_screening`, `consumer_written_instructions`, `legitimate_business_need`).
- **Use the standard Sex Offender and Criminal Search APIs** for everything that is not an eligibility decision about a consumer — community safety, platform trust and safety signals reviewed by your own team, research, and monitoring.
- **Not sure?** If a person could be turned down for housing or a job because of the result, run it as an FCRA order.

## The lifecycle of an order

1. **Register the end user** once — the employer or landlord the report is for, with its permissible purposes and your certification.
2. **Create the order** with the subject, the purpose, your consent attestation, where the job or housing is, and the searches to run. You get `202` and an order id at once.
3. **Each search runs** — live in the relevant jurisdictions and in our data elsewhere, matched, filtered through the controls, re-verified — and moves from `queued` to `complete` (or `unavailable` when its sources cannot be reached). The order is `partial` while some searches are done and others are not.
4. **You are told** by `search.completed` and `order.completed` webhooks, or you poll the order.
5. **You read the results** — the furnished records with their match evidence, and the counts of what was withheld and why — and adjudicate under your own process.
6. **If the consumer disputes**, through you or the consumer portal, a reinvestigation runs on the 30-day clock; you receive the outcome, and a correction or deletion reaches every partner that received the record.

## Your first order

POST /v1/fcra/orders

```bash
curl -X POST https://api.offendersearch.app/v1/fcra/orders \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Idempotency-Key: 8f1c2a7e-order-889" \
  -H "Content-Type: application/json" \
  -d '{
    "endUserId": "eu_0b44acbeefeaa7d7",
    "permissiblePurpose": "employment",
    "consent": { "obtainedAt": "2026-10-02T13:55:00Z", "method": "esign", "documentRef": "auth-55120" },
    "subject": {
      "firstName": "Jordan", "lastName": "Example", "dob": "1984-02-11",
      "aliases": [{ "firstName": "Jordan", "lastName": "Sample" }],
      "addresses": [{ "state": "IL", "city": "Springfield", "from": "2019-01", "to": null }]
    },
    "useLocation": { "state": "IL" },
    "liveScope": "relevant",
    "searches": [{ "type": "sex_offender" }, { "type": "criminal", "exclusions": "all" }],
    "clientReference": "cra-order-889",
    "callbackUrl": "https://your-cra.example/hooks/offendersearch"
  }'
```

## Endpoint index

| Method | Path | What it does |
| --- | --- | --- |
| POST | `/v1/fcra/end-users` | Register an employer, landlord or other end user, with its permissible purposes and your certification. |
| GET | `/v1/fcra/end-users/{id}` | Retrieve an end user. `PATCH` updates its purposes or suspends it. |
| POST | `/v1/fcra/orders` | Create an order with a sex offender search, a criminal search, or both. Returns 202. |
| GET | `/v1/fcra/orders/{id}` | The order, its status, and the status and result of each search. |
| POST | `/v1/fcra/orders/{id}/cancel` | Cancel an order that has not completed. |
| GET | `/v1/fcra/orders/{id}/searches/{searchId}` | One search with the records furnished. |
| POST | `/v1/fcra/searches/{searchId}/reverify` | Run a search again; the old one is marked superseded. |
| GET | `/v1/fcra/orders/{id}/report` | The furnished report, as JSON or the stored PDF. |
| GET | `/v1/fcra/orders/{id}/evidence` | Every evidence capture taken for the order’s live checks, with its hash. |
| GET | `/v1/fcra/orders/{id}/evidence/{captureId}` | One capture’s stored bytes, with its hash and its kind. |
| GET | `/v1/fcra/receipts/{orderId}` | A public signed receipt for a completed order. No key, no personal data. |
| POST | `/v1/fcra/orders/{id}/disputes` | Open a dispute on furnished records. |
| GET | `/v1/fcra/disputes/{id}` | A dispute, its due date and its outcome. `GET /v1/fcra/disputes` lists them. |
| POST | `/v1/fcra/disputes/{id}/information` | Add the consumer’s new information — the one 15-day extension. |
| PUT | `/v1/fcra/webhook-endpoint` | Set your webhook URL and get its signing secret. `GET` and `DELETE` too. |
| GET | `/v1/fcra/events` | Every event for your account, to poll when a webhook is missed. |
| POST | `/v1/fcra/events/{id}/resend` | Deliver one event to your webhook again. |

---

# Access & end users

> How FCRA Partner Access is enabled, which permissible purposes an agreement covers, and how to register the employers and landlords your orders are for.

HTML: https://offendersearch.app/docs/fcra/access · Markdown: https://offendersearch.app/docs/fcra/access.md

## FCRA Partner Access

The FCRA API is not self-serve. There is no dashboard switch, plan or key setting that turns it on. Our team enables it per account after a written FCRA agreement is signed; the agreement names the permissible purposes your account may use. Trial accounts cannot use it. Every call is authenticated with the same `X-API-Key` header as the other APIs.

Without access, every `/v1/fcra/*` call except `GET /v1/fcra/partner` and the public receipt returns `403 fcra_not_enabled`; an account that has access but is still on a trial gets `403 fcra_not_on_trial`. `GET /v1/fcra/partner` never returns 403: it reports `enabled: false` (with `trial: true` on a trial account) so your integration can tell. Request access at `/contact?topic=fcra`.

## FCRA Partner Access status API — GET /v1/fcra/partner

`GET /v1/fcra/partner` · auth: `X-API-Key`

Whether this account can place FCRA orders, the purposes its agreement covers, and whether a webhook endpoint is set. Never 403: an account without access gets `enabled: false`.

### Request

No parameters and no body beyond the `X-API-Key` header.

### Response

**200** (application/json) — The account's FCRA status.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` |  | `true` when this account can place FCRA orders now: an active FCRA partner agreement and not a trial account. `false` otherwise — show the request-access path. |
| `sandbox` | `boolean` |  | `true` when the partner account is in sandbox: orders return deterministic test answers, make no live calls and are never metered. `false` without access. |
| `allowedPurposes` | `array of string` — one of `"employment"`, `"tenant_screening"`, `"consumer_written_instructions"`, `"legitimate_business_need"` |  | The permissible purposes your partner agreement covers. An end user and an order may use only these. `[]` without access. |
| `agreementRef` | `string` or null |  | The reference of your signed partner agreement. `null` without access. |
| `enabledAt` | `string (date-time)` or null |  | When FCRA Partner Access was enabled for this account. `null` without access. |
| `webhookConfigured` | `boolean` |  | `true` when a webhook endpoint is set with `PUT /v1/fcra/webhook-endpoint`. |
| `trial` | `boolean` |  | `true` on a trial account. FCRA orders are not available on a trial account. |
| `role` | `string` — one of `"admin"`, `"user"` |  | Who is calling: `admin` for an API key and for an account admin's dashboard session, `user` for a team member's session. Only `admin` can change the webhook endpoint. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |

## Permissible purposes

| Purpose | Meaning |
| --- | --- |
| `employment` | Employment purposes, with your end user’s § 1681b(b) certifications in place. |
| `tenant_screening` | A rental application the consumer initiated. |
| `consumer_written_instructions` | The consumer’s written instructions (§ 1681b(a)(2)). |
| `legitimate_business_need` | Another business transaction the consumer initiated. |

An end user may hold only purposes your agreement allows, and an order may state only a purpose its end user holds and your agreement still allows.

## End users

An end user is the employer, landlord or other business the report is for. Register each one once, with your certification that it holds the stated purposes, and reference it by id on every order.

POST /v1/fcra/end-users

```bash
curl -X POST https://api.offendersearch.app/v1/fcra/end-users \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Northwind Staffing LLC",
    "externalId": "EU-4821",
    "permissiblePurposes": ["employment"],
    "certification": {
      "text": "End user certifies it will use reports only for employment purposes …",
      "version": "v3",
      "certifiedAt": "2026-10-02T14:00:00Z",
      "certifiedBy": "compliance@your-cra.example"
    }
  }'
```

`GET /v1/fcra/end-users` lists them; `GET /v1/fcra/end-users/{id}` returns one; `PATCH /v1/fcra/end-users/{id}` changes its purposes, or sets `status` to `suspended` or back to `active`. A suspended end user cannot be used on a new order (`422 end_user_suspended`). Every field each of these calls takes and returns is in the end user API reference below.

## Register an FCRA end user — POST /v1/fcra/end-users request and response

`POST /v1/fcra/end-users` · auth: `X-API-Key`

Registers the employer, landlord or other business a report is for, once, with the permissible purposes it holds and your certification.

### Request

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `name` | `string` | required | The end user's legal name, 1 to 200 characters. |
| `externalId` | `string` or null | optional | Your own id for the end user, up to 200 characters. Returned as sent. |
| `address` | [Address](https://offendersearch.app/docs/fcra/access#object-address) or null | optional | The end user's business address. |
| `permissiblePurposes` | `array of string` — one of `"employment"`, `"tenant_screening"`, `"consumer_written_instructions"`, `"legitimate_business_need"` | required | One or more purposes the end user holds, every one within your partner agreement — otherwise 403 `fcra_purpose_not_allowed`. |
| `certification` | [Certification](https://offendersearch.app/docs/fcra/access#object-certification) | required | Your record of the end user's certification. |

### Response

**201** (application/json) — The end user, status `active`.

The body is [the `EndUser` object](https://offendersearch.app/docs/fcra/access#object-enduser). An employer, landlord or other business your orders are for. Every order names one by `id`.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 403 | `fcra_purpose_not_allowed` | A purpose is not covered by your partner agreement. |
| 422 | — | A field failed validation (the standard validation body). |

## The EndUser object — FCRA end user fields

An employer, landlord or other business your orders are for. Every order names one by `id`.

Returned or accepted by [`POST /v1/fcra/end-users`](https://offendersearch.app/docs/fcra/access#reference-post-v1-fcra-end-users), [`GET /v1/fcra/end-users`](https://offendersearch.app/docs/fcra/access#reference-get-v1-fcra-end-users), [`GET /v1/fcra/end-users/{end_user_id}`](https://offendersearch.app/docs/fcra/access#reference-get-v1-fcra-end-users-end-user-id), [`PATCH /v1/fcra/end-users/{end_user_id}`](https://offendersearch.app/docs/fcra/access#reference-patch-v1-fcra-end-users-end-user-id).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The end user's id. Send it as `endUserId` on every order for this business. Example `"eu_0b44acbeefeaa7d7"`. |
| `name` | `string` |  | The end user's legal name. |
| `externalId` | `string` or null |  | Your own id for the end user. `null` when you did not send one. |
| `address` | [Address](https://offendersearch.app/docs/fcra/access#object-address) or null |  | The business address. `null` when you did not send one. |
| `permissiblePurposes` | `array of string` — one of `"employment"`, `"tenant_screening"`, `"consumer_written_instructions"`, `"legitimate_business_need"` |  | The purposes this end user holds. An order may state only one of these. |
| `certification` | [Certification](https://offendersearch.app/docs/fcra/access#object-certification) |  | The certification you recorded. |
| `status` | `string` — one of `"active"`, `"suspended"` |  | `active`, or `suspended` — a suspended end user cannot be named on a new order (422 `end_user_suspended`). |
| `createdAt` | `string (date-time)` |  | When the end user was registered. |
| `updatedAt` | `string (date-time)` |  | When it last changed. |

## The Address object — end user address fields

An end user's business address. Every field is optional; `state` is a two-letter code and is returned upper-cased.

Returned or accepted by [`POST /v1/fcra/end-users`](https://offendersearch.app/docs/fcra/access#reference-post-v1-fcra-end-users), [`GET /v1/fcra/end-users`](https://offendersearch.app/docs/fcra/access#reference-get-v1-fcra-end-users), [`GET /v1/fcra/end-users/{end_user_id}`](https://offendersearch.app/docs/fcra/access#reference-get-v1-fcra-end-users-end-user-id), [`PATCH /v1/fcra/end-users/{end_user_id}`](https://offendersearch.app/docs/fcra/access#reference-patch-v1-fcra-end-users-end-user-id).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `line1` | `string` or null | optional | Street line, up to 200 characters. |
| `city` | `string` or null | optional | City, up to 100 characters. |
| `state` | `string` or null | optional | Two-letter state code, any case; returned upper-cased. Anything else is a 422. |
| `postalCode` | `string` or null | optional | Postal code, up to 20 characters. |

## The Certification object — end user certification fields

The certification the end user gave you that it holds the stated purposes, recorded as you hold it.

Returned or accepted by [`POST /v1/fcra/end-users`](https://offendersearch.app/docs/fcra/access#reference-post-v1-fcra-end-users), [`GET /v1/fcra/end-users`](https://offendersearch.app/docs/fcra/access#reference-get-v1-fcra-end-users), [`GET /v1/fcra/end-users/{end_user_id}`](https://offendersearch.app/docs/fcra/access#reference-get-v1-fcra-end-users-end-user-id), [`PATCH /v1/fcra/end-users/{end_user_id}`](https://offendersearch.app/docs/fcra/access#reference-patch-v1-fcra-end-users-end-user-id).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `text` | `string` | required | The certification as the end user gave it to you, 10 to 8,000 characters. |
| `version` | `string` | required | Your version label for that certification text, 1 to 50 characters. |
| `certifiedAt` | `string (date-time)` | required | When the certification was given, ISO 8601. A time more than five minutes in the future is a 422. Returned normalised with a UTC offset. |
| `certifiedBy` | `string` or null | optional | Who at your company recorded it, up to 200 characters. |

## List FCRA end users — GET /v1/fcra/end-users response

`GET /v1/fcra/end-users` · auth: `X-API-Key`

Every end user on this account.

### Request

No parameters and no body beyond the `X-API-Key` header.

### Response

**200** (application/json) — The end users.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `endUsers` | array of [EndUser](https://offendersearch.app/docs/fcra/access#object-enduser) |  | Every end user on the account. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |

## Retrieve an FCRA end user — GET /v1/fcra/end-users/{end_user_id}

`GET /v1/fcra/end-users/{end_user_id}` · auth: `X-API-Key`

One end user.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `end_user_id` | `string` | required | The end user id (`eu_…`). Example `"eu_0b44acbeefeaa7d7"`. |

### Response

**200** (application/json) — The end user.

The body is [the `EndUser` object](https://offendersearch.app/docs/fcra/access#object-enduser). An employer, landlord or other business your orders are for. Every order names one by `id`.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such end user on this account. |

## Update or suspend an FCRA end user — PATCH /v1/fcra/end-users/{end_user_id}

`PATCH /v1/fcra/end-users/{end_user_id}` · auth: `X-API-Key`

Changes an end user's purposes, or suspends or reinstates it.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `end_user_id` | `string` | required | The end user id (`eu_…`). Example `"eu_0b44acbeefeaa7d7"`. |

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `permissiblePurposes` | `array of string` or null — one of `"employment"`, `"tenant_screening"`, `"consumer_written_instructions"`, `"legitimate_business_need"` | optional | Replaces the end user's purposes. At least one; every one within your partner agreement. |
| `status` | `string` or null — one of `"active"`, `"suspended"` | optional | `suspended` stops the end user being named on new orders; `active` reinstates it. Existing orders are unaffected. |

### Response

**200** (application/json) — The updated end user.

The body is [the `EndUser` object](https://offendersearch.app/docs/fcra/access#object-enduser). An employer, landlord or other business your orders are for. Every order names one by `id`.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 403 | `fcra_purpose_not_allowed` | A purpose is not covered by your partner agreement. |
| 404 | `not_found` | No such end user on this account. |

## What stays with you

- Disclosure, authorization and consent from the consumer — you attest to it on each order.
- Vetting your end users and holding their certifications.
- Adjudication, pre-adverse and adverse-action notices, and the consumer’s copy of the report.

Integrating this API does not by itself make your workflow compliant. It gives you the records, the controls that ran, the evidence and the dispute channel; whether a given use is permitted, and every notice and certification around it, is governed by your agreement and your own compliance review.

---

# FCRA API sandbox & test subjects

> Test FCRA orders end to end without live calls or charges: on a sandbox account, test subjects return clear, records found, withheld, incomplete or a dispute.

HTML: https://offendersearch.app/docs/fcra/sandbox · Markdown: https://offendersearch.app/docs/fcra/sandbox.md

## Test without live calls

Sandbox orders run the full order, search, dispute and webhook flow with deterministic results, no live calls and no charges. The subject’s **last name** picks the outcome; every test subject is listed under FCRA API sandbox orders below, with what it returns.

Any other last name returns `clear`. Sandbox orders are never metered, and every sandbox record carries `sandbox: true` and an id beginning `sandbox:`.

Two ways to use these rules. On the live API, sandbox is a setting on your partner account, enabled by our team — ask for it when you request FCRA Partner Access. On the sandbox server, with a test key, every account is already a sandbox partner for every permissible purpose, and orders follow the same last-name rules. The rest of this page is the sandbox server.

## FCRA API test keys: create one in the dashboard

The FCRA sandbox server is the FCRA API on a separate server. With a test key, every account is already a sandbox FCRA partner for every permissible purpose, so there is nothing to request first: register an end user, place orders, open disputes and read webhook events, with the same paths and shapes as live. Nothing is billed.

1. In the dashboard, open [Developers → API keys](https://offendersearch.app/dashboard/keys).
2. Choose **Create test key**. Test keys are for business accounts and need no subscription.
3. Copy the key. Test keys begin `os_test_`. Like a live key, the full secret is shown once, so put it in your secrets store straight away.

One test key works on every API’s sandbox. Each key in the list is labelled **Test · Sandbox** or **Live**, with the base URL it works on. Send it in the same `X-API-Key` header the live API uses. An account holds up to 3 active test keys and can create 10 a day. A revoked test key stops working on the sandbox within a few minutes.

## FCRA API sandbox base URL vs live

|  | Sandbox | Production |
| --- | --- | --- |
| Base URL | `https://sandbox.offendersearch.app` | `https://api.offendersearch.app` |
| Key prefix | `os_test_` | `os_live_` |
| Data | Outcomes picked by the subject’s last name, listed below | Live FCRA orders under your partner agreement |
| Billing | Never billed, never recorded to usage | Your plan’s prices |
| `sandbox` field | `true` on every response | Absent |
| `X-Offendersearch-Environment` | `sandbox` on every response | Absent |

Each key works on its own server only, so a mixed-up configuration is caught on the first request rather than after a release. A test key sent to the live API, or a live key sent to the sandbox, is refused with `401` and a `detail` message that says so:

401 — a test key sent to the live API

```json
{
  "detail": "This is a test key (os_test_…). Test keys work only on the sandbox at https://sandbox.offendersearch.app, which answers with sample data. To search real records, send a live key (os_live_…) to https://api.offendersearch.app."
}
```

401 — a live key sent to the sandbox

```json
{
  "detail": "This is a live key. The sandbox at https://sandbox.offendersearch.app accepts test keys (os_test_…) only — create one in the dashboard under Developers → API keys. Send live keys to https://api.offendersearch.app."
}
```

A revoked test key gets `401` with: “This test key has been revoked. Create a new test key in the dashboard under Developers → API keys.”

Each test key can send 60 requests a minute and 2,000 a day, and one IP address 120 a minute, across every API on the sandbox. Past a limit the sandbox answers `429` with a `Retry-After` header. See [Sandbox rate limits](https://offendersearch.app/docs/sandbox.md#rate-limits).

## Register an FCRA end user in the sandbox

Every order names an end user: the employer or landlord the report is for. Register one once with `POST /v1/fcra/end-users`, keep its `id`, and send it as `endUserId` on every sandbox order. The fields are the same as live; see [Access & end users](https://offendersearch.app/docs/fcra/access.md).

1. POST /v1/fcra/end-users — sandbox

```bash
curl -X POST "https://sandbox.offendersearch.app/v1/fcra/end-users" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "Sample Staffing LLC",
      "permissiblePurposes": [
        "employment"
      ],
      "certification": {
        "text": "End user certifies it will use reports only for employment purposes.",
        "version": "v1",
        "certifiedAt": "2026-10-01T12:00:00Z"
      }
    }'
```

## FCRA API sandbox orders

The subject’s **last name** picks the outcome, and for `Excluded` and `Sanctioned` the criminal search’s `exclusions` mode picks it too. An order finishes right after the `202`: read it once with `GET /v1/fcra/orders/{orderId}`. Sandbox records carry `sandbox: true` and ids beginning `sandbox:`.

| Send | What comes back |
| --- | --- |
| `subject.lastName: Clear`, `searches: sex_offender, criminal` | **Clear (any other last name).** Order complete; every search complete with result clear and no records. Any last name not listed here behaves the same way. |
| `subject.lastName: Records`, `searches: sex_offender, criminal` | **Records found.** Order complete; each search furnishes one sandbox record (records_found). The sex offender record carries registry{}, the criminal record cases[] with a misdemeanor conviction. |
| `subject.lastName: Withheld`, `searches: criminal` | **Records withheld.** Search complete with result clear: nothing furnished, only withheld counts (2 insufficient_identifiers, 1 not_current_at_source). |
| `subject.lastName: Unavailable`, `searches: sex_offender` | **Sources unavailable.** The search ends unavailable with result incomplete (the sources could not be reached), and the order completes. This is production's documented sandbox outcome for an FCRA order, not a simulated outage of the sandbox. |
| `subject.lastName: Dispute`, `searches: criminal` | **Dispute that resolves.** One furnished record, as Records. A dispute filed on it (POST /v1/fcra/orders/{id}/disputes) resolves deleted 60 seconds later, read on the next GET of the dispute; dispute.created and dispute.updated events are emitted. |
| `subject.lastName: Excluded`, `searches: criminal` | **Conviction-based exclusion.** The criminal search furnishes one exclusion-list record (recordType healthcare_exclusion, dispositionStatus conviction, authority 1128(a)(1)). A conviction-based exclusion is furnished under the default exclusions mode, conviction_only. |
| `subject.lastName: Excluded`, `searches: criminal (exclusions: none)` | **Conviction-based exclusion, exclusions none.** The same subject with exclusions: "none": no exclusion-list record is furnished or counted, so the search is clear. |
| `subject.lastName: Sanctioned`, `searches: criminal` | **Administrative exclusion, default mode.** An administrative exclusion (license action, not a conviction) is not furnished under the default conviction_only mode: the search is clear. |
| `subject.lastName: Sanctioned`, `searches: criminal (exclusions: all)` | **Administrative exclusion, exclusions all.** With exclusions: "all" the administrative exclusion is furnished: one record, dispositionStatus sanction, convictionBased false. |

2. POST /v1/fcra/orders — sandbox (use the end user’s id)

```bash
curl -X POST "https://sandbox.offendersearch.app/v1/fcra/orders" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "endUserId": "{endUserId}",
      "permissiblePurpose": "employment",
      "consent": {
        "obtainedAt": "2026-10-01T12:00:00Z",
        "method": "esign"
      },
      "subject": {
        "firstName": "Pat",
        "lastName": "Records",
        "dob": "1980-01-01"
      },
      "searches": [
        {
          "type": "sex_offender"
        },
        {
          "type": "criminal"
        }
      ]
    }'
```

## FCRA API sandbox examples in cURL, Node and Python

Records found: an order for subject.lastName Records, after you register an end user. Put its id in endUserId. Export your test key as `OFFENDERSEARCH_TEST_KEY` first.

POST /v1/fcra/orders — sandbox — cURL

```bash
curl -X POST "https://sandbox.offendersearch.app/v1/fcra/orders" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "endUserId": "{endUserId}",
      "permissiblePurpose": "employment",
      "consent": {
        "obtainedAt": "2026-10-01T12:00:00Z",
        "method": "esign"
      },
      "subject": {
        "firstName": "Pat",
        "lastName": "Records",
        "dob": "1980-01-01"
      },
      "searches": [
        {
          "type": "sex_offender"
        },
        {
          "type": "criminal"
        }
      ]
    }'
```

POST /v1/fcra/orders — sandbox — Node

```javascript
// Node 18+ — fetch is built in.
const BASE = process.env.OFFENDERSEARCH_BASE ?? 'https://sandbox.offendersearch.app';

const res = await fetch(`${BASE}/v1/fcra/orders`, {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.OFFENDERSEARCH_TEST_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "endUserId": "{endUserId}",
    "permissiblePurpose": "employment",
    "consent": {
      "obtainedAt": "2026-10-01T12:00:00Z",
      "method": "esign"
    },
    "subject": {
      "firstName": "Pat",
      "lastName": "Records",
      "dob": "1980-01-01"
    },
    "searches": [
      {
        "type": "sex_offender"
      },
      {
        "type": "criminal"
      }
    ]
  }),
});
const body = await res.json();

console.log(res.headers.get('X-Offendersearch-Environment')); // "sandbox"
console.log(res.status);  // 202
console.log(body.id);      // read it once with GET /v1/fcra/orders/{orderId}
```

POST /v1/fcra/orders — sandbox — Python

```python
import os
import requests

BASE = os.environ.get("OFFENDERSEARCH_BASE", "https://sandbox.offendersearch.app")

r = requests.post(
    f"{BASE}/v1/fcra/orders",
    headers={"X-API-Key": os.environ["OFFENDERSEARCH_TEST_KEY"]},
    json={
        "endUserId": "{endUserId}",
        "permissiblePurpose": "employment",
        "consent": {
            "obtainedAt": "2026-10-01T12:00:00Z",
            "method": "esign",
        },
        "subject": {
            "firstName": "Pat",
            "lastName": "Records",
            "dob": "1980-01-01",
        },
        "searches": [
            {
                "type": "sex_offender",
            },
            {
                "type": "criminal",
            },
        ],
    },
    timeout=30,
)
body = r.json()

print(r.headers["X-Offendersearch-Environment"])  # sandbox
print(r.status_code)  # 202
print(body["id"])      # read it once with GET /v1/fcra/orders/{orderId}
```

## Postman collection for the FCRA API sandbox

One collection covers every API’s sandbox, with a folder per API, ready to import into Postman or Insomnia: https://offendersearch.app/downloads/offendersearch-sandbox.postman_collection.json. The **FCRA API** folder has one request for every scenario on this page. Set `{{apiKey}}` to your test key; `{{baseUrl}}` is preset to the sandbox. Ids from earlier responses go in `{{endUserId}}`.

## Move FCRA orders from the sandbox to production

The sandbox and the live API differ only in the base URL and the key. Keep both in configuration, not in code, and going live is a configuration change:

.env

```bash
# Sandbox
OFFENDERSEARCH_BASE=https://sandbox.offendersearch.app
OFFENDERSEARCH_KEY=os_test_…

# Production — change these two values and nothing else
OFFENDERSEARCH_BASE=https://api.offendersearch.app
OFFENDERSEARCH_KEY=os_live_…
```

- Create a live key on the same [Developers → API keys](https://offendersearch.app/dashboard/keys) page.
- Remove any branch that reads `sandbox`. The field does not exist on live.
- Live FCRA orders need FCRA Partner Access under a written agreement: see [Access & end users](https://offendersearch.app/docs/fcra/access.md). Register your live end users again with the live key.
- FCRA pricing is quoted per partner.

---

# Orders

> Create an FCRA order covering a sex offender search, a criminal search or both, with an idempotency key, then track it through received, partial and complete.

HTML: https://offendersearch.app/docs/fcra/orders · Markdown: https://offendersearch.app/docs/fcra/orders.md

## Create an order

`POST /v1/fcra/orders` returns **202** with the order at status `received`, and the work starts in the background. Send an `Idempotency-Key` header: the same key with the same body within 24 hours returns the original order (with the response header `Idempotent-Replayed: true`); the same key with a different body is `409 idempotency_conflict`. Every request field is in the order request reference below.

POST /v1/fcra/orders

```bash
curl -X POST https://api.offendersearch.app/v1/fcra/orders \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Idempotency-Key: 8f1c2a7e-order-889" \
  -H "Content-Type: application/json" \
  -d '{
    "endUserId": "eu_0b44acbeefeaa7d7",
    "permissiblePurpose": "employment",
    "consent": { "obtainedAt": "2026-10-02T13:55:00Z", "method": "esign", "documentRef": "auth-55120" },
    "subject": {
      "firstName": "Jordan", "lastName": "Example", "dob": "1984-02-11",
      "aliases": [{ "firstName": "Jordan", "lastName": "Sample" }],
      "addresses": [{ "state": "IL", "city": "Springfield", "from": "2019-01", "to": null }]
    },
    "useLocation": { "state": "IL" },
    "liveScope": "relevant",
    "searches": [{ "type": "sex_offender" }, { "type": "criminal", "exclusions": "all" }],
    "clientReference": "cra-order-889",
    "callbackUrl": "https://your-cra.example/hooks/offendersearch"
  }'
```

### Exclusion lists in a criminal search

A criminal search also covers the HHS-OIG, SAM.gov and state Medicaid exclusion lists — exclusion-list records with `recordType: "healthcare_exclusion"` and an `exclusion` block (see the criminal record object). The search’s `exclusions` option says which of them may be furnished: `conviction_only` (the default) furnishes exclusions whose basis is a criminal conviction, as convictions dated by the exclusion date; `all` also furnishes the administrative tier — license actions, debarments from federal awards, loan defaults — with `dispositionStatus: "sanction"`; `none` furnishes no exclusion-list records. A list entry with no date of birth is withheld as `insufficient_identifiers` whatever the setting. The option is in the SearchSpec reference below.

## Create an FCRA order API — POST /v1/fcra/orders request and response fields

`POST /v1/fcra/orders` · auth: `X-API-Key`

Creates an order for one consumer with a sex offender search, a criminal search or both. Returns 202 with the order at `received`; the searches run in the background and report by webhook.

### Request

**Headers**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | optional | Up to 200 characters. The same key with the same body within 24 hours returns the original order, with the response header `Idempotent-Replayed: true`; the same key with a different body is 409 `idempotency_conflict`. Example `"8f1c2a7e-order-889"`. |

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `endUserId` | `string` | required | A registered, active end user on this account. Unknown: 404 `unknown_end_user`; suspended: 422 `end_user_suspended`. Example `"eu_0b44acbeefeaa7d7"`. |
| `permissiblePurpose` | `string` — one of `"employment"`, `"tenant_screening"`, `"consumer_written_instructions"`, `"legitimate_business_need"` | required | One of `employment`, `tenant_screening`, `consumer_written_instructions` or `legitimate_business_need`. It must be held by the end user and covered by your agreement — otherwise 403 `fcra_purpose_not_allowed`, and the refusal is written to the evidence record. |
| `consent` | [Consent](https://offendersearch.app/docs/fcra/orders#object-consent) | required | Your consent attestation. |
| `subject` | [Subject](https://offendersearch.app/docs/fcra/orders#object-subject) | required | The consumer. |
| `useLocation` | [UseLocation](https://offendersearch.app/docs/fcra/orders#object-uselocation) or null | optional | Where the job or housing is. When omitted, only the states in `subject.addresses` (and any jurisdiction that produces an exact date-of-birth candidate) are searched live. |
| `annualSalary` | `integer` or null | optional | Employment only: the position's annual salary in US dollars, 0 to 100,000,000. At $75,000 or more the federal seven-year limit on non-conviction criminal items does not apply; a state limit still does. |
| `searches` | array of [SearchSpec](https://offendersearch.app/docs/fcra/orders#object-searchspec) | required | One or two searches, each type at most once: `[{"type":"sex_offender"}]`, `[{"type":"criminal"}]`, or both. |
| `clientReference` | `string` or null | optional | Your order reference, up to 200 characters. Returned on the order and in the report, and filterable on `GET /v1/fcra/orders`. Example `"cra-order-889"`. |
| `callbackUrl` | `string (uri)` or null | optional | An `https://` URL that receives this order's events instead of your webhook endpoint, signed with the same secret. Requires a configured webhook endpoint (422 `webhook_endpoint_required`). Example `"https://your-cra.example/hooks/offendersearch"`. |
| `liveScope` | `string` — one of `"relevant"`, `"all"` | optional · default `"relevant"` | `relevant`: each search runs live in every state in `subject.addresses`, in `useLocation.state`, and in every jurisdiction that produced an exact date-of-birth candidate. `all`: live in every source that supports a live search for that type. Every other source is searched in our data, and `coverage` says which was which. |

### Response

**202** (application/json) — The order, accepted (or, for a replayed `Idempotency-Key`, the original order).

The body is [the `Order` object](https://offendersearch.app/docs/fcra/orders#object-order). An FCRA order: its status, its end user and purpose, every search it carries, and the consumer portal access code.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 403 | `fcra_purpose_not_allowed` | The purpose is not held by the end user or not covered by your agreement. The refusal is written to the evidence record. |
| 404 | `unknown_end_user` | `endUserId` is not one of your end users. |
| 409 | `idempotency_conflict` | The `Idempotency-Key` was used with a different body. |
| 422 | `end_user_suspended` | The end user is suspended. |
| 422 | `webhook_endpoint_required` | `callbackUrl` was sent and no webhook endpoint is configured. |
| 422 | `validation` | The `Idempotency-Key` is over 200 characters. |
| 422 | — | A field failed validation — a partial or future date of birth, a future consent time, an unknown purpose, search type or `liveScope`, a repeated search type, or an unexpected field (the standard validation body). |

## The Consent object — FCRA consent attestation fields

Your attestation that you obtained the consumer's disclosure and authorization, recorded on the order and in its evidence record.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `obtainedAt` | `string (date-time)` | required | When you obtained the consumer's authorization, ISO 8601. More than five minutes in the future is a 422. Example `"2026-10-02T13:55:00Z"`. |
| `method` | `string` — one of `"esign"`, `"wet_signature"`, `"electronic_click"`, `"recorded_verbal"`, `"other"` | required | How it was obtained. |
| `documentRef` | `string` or null | optional | Your reference to the signed authorization, up to 200 characters. |

## The Subject object — FCRA order subject fields

The consumer the order is about. A full date of birth is required: the FCRA controls furnish exact date-of-birth matches only.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `firstName` | `string` | required | The consumer's first name, 1 to 100 characters. |
| `middleName` | `string` or null | optional | Middle name, up to 100 characters. Recorded in the evidence record. |
| `lastName` | `string` | required | The consumer's last name, 1 to 100 characters. The consumer portal matches on it. |
| `dob` | `string (date)` | required | A full, real, past date of birth, `YYYY-MM-DD`. A year, an age or a future date is a 422. Example `"1984-02-11"`. |
| `aliases` | array of [Alias](https://offendersearch.app/docs/fcra/orders#object-alias) or null | optional | Up to 5 other names. Each is searched. |
| `addresses` | array of [SubjectAddress](https://offendersearch.app/docs/fcra/orders#object-subjectaddress) or null | optional | Up to 20 addresses in the consumer's history. |

## The Alias object — subject alias fields

Another name the consumer has used. Every alias is searched in addition to the subject's name.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `firstName` | `string` | required | 1 to 100 characters. |
| `lastName` | `string` | required | 1 to 100 characters. |

## The SubjectAddress object — subject address history fields

One address in the consumer's history. Each state listed here is searched live (except a registry withheld for the order's purpose) and brings its reporting limits to the criminal search.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `state` | `string` | required | Two-letter state code, any case. Searched live (except a registry withheld for the order's purpose), and its seven-year conviction limit applies when it has one. Example `"IL"`. |
| `city` | `string` or null | optional | City, up to 100 characters. |
| `from` | `string` or null | optional | When the consumer moved in, as you hold it (for example `2019-01`), up to 10 characters. Example `"2019-01"`. |
| `to` | `string` or null | optional | When they moved out, same format. `null` for the current address. |

## The UseLocation object — job or housing state

Where the job or the housing is. Its state is searched live (except a registry withheld for the order's purpose) and its reporting limits apply.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `state` | `string` | required | Two-letter state code, any case. Example `"IL"`. |

## The SearchSpec object — FCRA search types

One search to run in the order.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `type` | `string` — one of `"sex_offender"`, `"criminal"` | required | The search to run. |
| `exclusions` | `string` — one of `"conviction_only"`, `"all"`, `"none"` | optional · default `"conviction_only"` | Criminal searches only: which [exclusion-list records](https://offendersearch.app/docs/criminal/record-object#exclusions) the search may furnish. `conviction_only` (the default): only exclusions whose basis is a criminal conviction, furnished as convictions dated by the exclusion date. `all`: also the administrative tier — license actions, debarments, loan defaults — furnished with `dispositionStatus: "sanction"`. `none`: no exclusion-list records. A list entry with no date of birth is withheld as `insufficient_identifiers` whatever the setting. |

## The order object

GET /v1/fcra/orders/{id}

```json
{
  "id": "fo_ee0b34c5b52fd35b",
  "status": "in_progress",
  "createdAt": "2026-10-02T14:01:07Z",
  "updatedAt": "2026-10-02T14:01:51Z",
  "completedAt": null,
  "cancelledAt": null,
  "clientReference": "cra-order-889",
  "endUserId": "eu_0b44acbeefeaa7d7",
  "permissiblePurpose": "employment",
  "useLocation": { "state": "IL" },
  "liveScope": "relevant",
  "controlVersion": "fcra-controls-2026-10-19.1",
  "sandbox": false,
  "eta": { "p50": "2026-10-02T14:03:00Z", "p90": "2026-10-02T14:09:00Z" },
  "searches": [
    { "id": "fs_452884eecde9a1f2", "type": "sex_offender", "status": "complete", "result": "clear",
      "recordCount": 0, "withheld": { "insufficient_identifiers": 2 },
      "createdAt": "2026-10-02T14:01:07Z", "startedAt": "2026-10-02T14:01:08Z",
      "completedAt": "2026-10-02T14:01:51Z", "supersededBy": null,
      "coverage": [ … ], "incompleteReasons": [] },
    { "id": "fs_9c03d7e1a4b6f258", "type": "criminal", "status": "in_progress", "result": null,
      "recordCount": 0, "withheld": {},
      "createdAt": "2026-10-02T14:01:07Z", "startedAt": "2026-10-02T14:01:09Z",
      "completedAt": null, "supersededBy": null,
      "coverage": [], "incompleteReasons": [] }
  ],
  "consumerPortal": {
    "url": "https://offendersearch.app/consumer/report",
    "accessCode": "K7QF-2MXP-9RDA"
  }
}
```

| Order status | Meaning |
| --- | --- |
| `received` | Accepted; nothing has run yet. |
| `in_progress` | At least one search is running. |
| `partial` | Some searches are complete and others are not. |
| `complete` | Every search has finished. Read each search’s `result`. |
| `cancelled` | Cancelled before it completed. |
| `error` | Every search in the order errored. Nothing was furnished; place a new order. |

`eta` gives the 50th and 90th percentile completion times for the searches still running, from the last seven days of completions of each search type (a fixed estimate until five completions exist). It is `null` once the order is `complete`, `cancelled` or `error`.

## The Order object — FCRA order response fields

An FCRA order: its status, its end user and purpose, every search it carries, and the consumer portal access code.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders), [`GET /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders), [`GET /v1/fcra/orders/{order_id}`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders-order-id), [`POST /v1/fcra/orders/{order_id}/cancel`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders-order-id-cancel).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The order id. Use it on every other order route. Example `"fo_ee0b34c5b52fd35b"`. |
| `status` | `string` — one of `"received"`, `"in_progress"`, `"partial"`, `"complete"`, `"cancelled"`, `"error"` |  | `received` (nothing has run), `in_progress` (a search is running), `partial` (some searches are finished, others are not), `complete` (every search finished — read each search's `result`), `cancelled`, or `error` (every search errored; nothing was furnished — place a new order). |
| `createdAt` | `string (date-time)` |  | When the order was accepted. |
| `updatedAt` | `string (date-time)` |  | When anything on the order last changed. |
| `completedAt` | `string (date-time)` or null |  | When the last search finished. `null` until the order is `complete` or `error`. |
| `cancelledAt` | `string (date-time)` or null |  | When it was cancelled. `null` unless `status` is `cancelled`. |
| `clientReference` | `string` or null |  | Your reference, as sent. `null` when you sent none. |
| `endUserId` | `string` |  | The end user the order is for. |
| `permissiblePurpose` | `string` — one of `"employment"`, `"tenant_screening"`, `"consumer_written_instructions"`, `"legitimate_business_need"` |  | The purpose stated on the order. |
| `useLocation` | `object` or null |  | The job or housing location, as sent. `null` when you sent none. |
| `useLocation.state` | `string` |  | Two-letter state code, upper-cased. |
| `controlVersion` | `string` |  | The version of the controls this order runs under. The evidence record pins a hash of those exact rules. Example `"fcra-controls-2026-10-19.1"`. |
| `liveScope` | `string` — one of `"relevant"`, `"all"` |  | The live scope the order runs with. |
| `eta` | `object` or null |  | When the searches still running are expected to finish. `null` once the order is `complete`, `cancelled` or `error`, or when no search is queued or running. |
| `eta.p50` | `string (date-time)` |  | Median expected completion, from the last seven days of completions of each search type still running (a fixed estimate until five completions exist), measured from when the search was created. |
| `eta.p90` | `string (date-time)` |  | 90th-percentile expected completion, on the same basis. |
| `sandbox` | `boolean` |  | `true` for a sandbox order: deterministic answers, no live calls, never metered. |
| `searches` | array of [Search](https://offendersearch.app/docs/fcra/results#object-search) |  | Every search on the order, superseded ones included. These carry no `records`; read one search to get them. |
| `consumerPortal` | `object` |  | What the consumer needs to see this order's file. Shown only to the account that placed the order. |
| `consumerPortal.url` | `string (uri)` |  | The consumer portal address. Put it in the consumer's copy of the report or your pre-adverse notice. |
| `consumerPortal.accessCode` | `string` |  | The order's access code: 12 characters in three groups of four, with no `0`, `O`, `1`, `I` or `L`. The consumer may type it with or without dashes, in any case. Example `"K7QF-2MXP-9RDA"`. |

## Retrieve an FCRA order — GET /v1/fcra/orders/{order_id}

`GET /v1/fcra/orders/{order_id}` · auth: `X-API-Key`

The order, its status, and the status and result of each search.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `order_id` | `string` | required | The order id (`fo_…`). Example `"fo_ee0b34c5b52fd35b"`. |

### Response

**200** (application/json) — The order.

The body is [the `Order` object](https://offendersearch.app/docs/fcra/orders#object-order). An FCRA order: its status, its end user and purpose, every search it carries, and the consumer portal access code.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such order on this account. |

## List and cancel

`GET /v1/fcra/orders?status=&clientReference=&cursor=&limit=` lists your orders, newest first, 50 to a page by default (up to 200); pass the response’s `nextCursor` as `cursor` for the next page. `POST /v1/fcra/orders/{id}/cancel` cancels an order that has not finished — its queued searches become `cancelled`, and a search already running finishes — and returns `409 not_cancellable` for an order that is already `complete`, `error` or `cancelled`.

## List FCRA orders — GET /v1/fcra/orders query parameters and response

`GET /v1/fcra/orders` · auth: `X-API-Key`

Your orders, newest first, one page at a time.

### Request

**Query parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `status` | `string` — one of `"received"`, `"in_progress"`, `"partial"`, `"complete"`, `"cancelled"`, `"error"` | optional | Only orders with this status. |
| `clientReference` | `string` | optional | Only orders with this `clientReference`. |
| `cursor` | `string` | optional | The `nextCursor` from the previous page. |
| `limit` | `integer` | optional · default `50` | Orders per page, 1 to 200. |

### Response

**200** (application/json) — One page of orders.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `orders` | array of [Order](https://offendersearch.app/docs/fcra/orders#object-order) |  | The orders, newest first. |
| `nextCursor` | `string` or null |  | Send as `cursor` for the next page. `null` when this page holds fewer than `limit` orders. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |

## Cancel an FCRA order — POST /v1/fcra/orders/{order_id}/cancel

`POST /v1/fcra/orders/{order_id}/cancel` · auth: `X-API-Key`

Cancels an order that has not finished. Its queued searches become `cancelled`; a search already running finishes.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `order_id` | `string` | required | The order id (`fo_…`). Example `"fo_ee0b34c5b52fd35b"`. |

### Response

**200** (application/json) — The cancelled order.

The body is [the `Order` object](https://offendersearch.app/docs/fcra/orders#object-order). An FCRA order: its status, its end user and purpose, every search it carries, and the consumer portal access code.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such order on this account. |
| 409 | `not_cancellable` | The order is already `complete`, `error` or `cancelled`. |

## Test FCRA orders on the sandbox server

With a test key (os_test_…) every account is a sandbox FCRA partner on the sandbox server, with the same last-name rules. Register an end user, then place orders against it; nothing is billed. An order finishes right after the 202: read it once with GET /v1/fcra/orders/{orderId}.

- **Sandbox base URL:** `https://sandbox.offendersearch.app`. Not `https://api.offendersearch.app`, which takes live keys only.
- **Test key:** in the dashboard, Developers → API keys (https://offendersearch.app/dashboard/keys) → **Create test key**. Test keys begin `os_test_`; send yours in the `X-API-Key` header.

1. POST /v1/fcra/end-users

```bash
curl -X POST "https://sandbox.offendersearch.app/v1/fcra/end-users" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "name": "Sample Staffing LLC",
      "permissiblePurposes": [
        "employment"
      ],
      "certification": {
        "text": "End user certifies it will use reports only for employment purposes.",
        "version": "v1",
        "certifiedAt": "2026-10-01T12:00:00Z"
      }
    }'
```

2. POST /v1/fcra/orders

```bash
curl -X POST "https://sandbox.offendersearch.app/v1/fcra/orders" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "endUserId": "{endUserId}",
      "permissiblePurpose": "employment",
      "consent": {
        "obtainedAt": "2026-10-01T12:00:00Z",
        "method": "esign"
      },
      "subject": {
        "firstName": "Pat",
        "lastName": "Records",
        "dob": "1980-01-01"
      },
      "searches": [
        {
          "type": "sex_offender"
        },
        {
          "type": "criminal"
        }
      ]
    }'
```

- Every test name: [FCRA API sandbox orders](https://offendersearch.app/docs/fcra/sandbox#fcra)
- Postman collection: https://offendersearch.app/downloads/offendersearch-sandbox.postman_collection.json
- FCRA API sandbox: https://offendersearch.app/docs/fcra/sandbox.md
- Run any scenario from the browser: the HTML page's "Try it here" explorer (sandbox host and `os_test_` keys only, never billed).

---

# Results & records

> Read each search in an order: its status and result, the source-by-source coverage behind it, the records furnished with match evidence, and withheld counts.

HTML: https://offendersearch.app/docs/fcra/results · Markdown: https://offendersearch.app/docs/fcra/results.md

## The search object

Every search in an order has its own status and result. `GET /v1/fcra/orders/{id}/searches/{searchId}` returns it with the records furnished.

GET /v1/fcra/orders/{id}/searches/{searchId}

```json
{
  "id": "fs_9c03d7e1a4b6f258",
  "type": "criminal",
  "status": "complete",
  "result": "records_found",
  "recordCount": 1,
  "withheld": { "insufficient_identifiers": 1 },
  "createdAt": "2026-10-02T14:01:07Z",
  "startedAt": "2026-10-02T14:01:08Z",
  "completedAt": "2026-10-02T14:04:12Z",
  "supersededBy": null,
  "coverage": [
    { "source": "IL-SANGAMON", "name": "Sangamon County Circuit Court", "jurisdiction": "IL",
      "mode": "live", "checkedAt": "2026-10-02T14:02:38Z", "dataAsOf": null,
      "status": "answered", "relevant": true, "restricted": false,
      "candidates": 2, "furnished": 1, "withheld": 1, "captureIds": ["fc_41a2c7e09b3d5a18"] },
    { "source": "IL-DOC", "name": "Illinois Department of Corrections", "jurisdiction": "IL",
      "mode": "live", "checkedAt": "2026-10-02T14:02:51Z", "dataAsOf": null,
      "status": "answered", "relevant": true, "restricted": false,
      "candidates": 0, "furnished": 0, "withheld": 0, "captureIds": ["fc_41a2d1f6c8e0b274"] },
    { "source": "WI-CCAP", "name": "Wisconsin Circuit Court Access", "jurisdiction": "WI",
      "mode": "snapshot", "checkedAt": "2026-10-02T14:01:09Z", "dataAsOf": "2026-10-01",
      "status": "answered", "relevant": false, "restricted": false,
      "candidates": 0, "furnished": 0, "withheld": 0, "captureIds": [] }
  ],
  "incompleteReasons": [],
  "records": [
    {
      "recordId": "crim:IL-SANGAMON:2021CR004417",
      "type": "criminal",
      "matchBasis": { "state": "dob_match", "identifiers": ["name", "dob"] },
      "retrievedAt": "2026-10-02T14:02:40Z",
      "verifiedAt": "2026-10-02T14:04:09Z",
      "name": { "first": "JORDAN", "middle": "", "last": "EXAMPLE", "suffix": "" },
      "dob": { "date": "1984-02-11", "birthYear": 1984, "dobPrecision": "full", … },
      "sources": [{ "code": "IL-SANGAMON", "level": "county", "live": true, … }],
      "dispositionStatus": "conviction",
      "cases": [
        { "caseNumber": "2021CR004417", "court": "Sangamon County Circuit Court",
          "filedDate": "2021-06-14",
          "charges": [
            { "description": "Retail theft", "severity": "misdemeanor", "disposition": "Guilty",
              "dispositionDate": "2022-03-09", "fcraDisposition": "conviction" }
          ] }
      ],
      "fcraRedactions": { "obsolete_non_conviction": 1 },
      "…": "every other field of the criminal record object"
    }
  ]
}
```

| Search status | Meaning |
| --- | --- |
| `queued` | Waiting to run. |
| `in_progress` | Matching, filtering and re-verifying. |
| `complete` | Finished; read `result`. |
| `unavailable` | No source for this search could be reached; the result is `incomplete`. |
| `error` | The search could not run. Nothing was furnished. |
| `cancelled` | The order was cancelled before this search ran. |
| `superseded` | Replaced by a re-verified search; see `supersededBy`. |

| Result | Meaning |
| --- | --- |
| `records_found` | Every source answered, every relevant jurisdiction was searched live, every re-verification got an answer, and at least one record was furnished. |
| `clear` | Every relevant jurisdiction was searched **live** and answered, every other source answered, and nothing reportable was found. Never returned on a partial search. (Registries withheld for the order’s purpose are not searched live and do not make a result incomplete.) |
| `incomplete` | A source did not answer, a relevant jurisdiction could not be searched live (`live_unavailable` in `incompleteReasons`), or a record could not be re-verified. Records that passed every control are still furnished; treat the answer as a lower bound. |

## The Search object — FCRA search result fields

One search in an order: its status, its result, the source-by-source coverage behind it, and the counts of records withheld by reason.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders), [`GET /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders), [`GET /v1/fcra/orders/{order_id}`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders-order-id), [`POST /v1/fcra/orders/{order_id}/cancel`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders-order-id-cancel), [`GET /v1/fcra/orders/{order_id}/searches/{search_id}`](https://offendersearch.app/docs/fcra/results#reference-get-v1-fcra-orders-order-id-searches-search-id), [`POST /v1/fcra/searches/{search_id}/reverify`](https://offendersearch.app/docs/fcra/results#reference-post-v1-fcra-searches-search-id-reverify), [`GET /v1/fcra/orders/{order_id}/report`](https://offendersearch.app/docs/fcra/results#reference-get-v1-fcra-orders-order-id-report).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The search id. Example `"fs_452884eecde9a1f2"`. |
| `type` | `string` — one of `"sex_offender"`, `"criminal"` |  | `sex_offender` or `criminal`. |
| `status` | `string` — one of `"queued"`, `"in_progress"`, `"complete"`, `"unavailable"`, `"error"`, `"cancelled"`, `"superseded"` |  | `queued`, `in_progress`, `complete`, `unavailable` (no source could be reached; `result` is `incomplete`), `error` (the search could not run; nothing furnished), `cancelled` (the order was cancelled before it ran) or `superseded` (replaced by a re-verified search — see `supersededBy`). |
| `result` | `string` or null — one of `"records_found"`, `"clear"`, `"incomplete"` |  | `records_found`, `clear` or `incomplete`. `null` until the search finishes. `clear` means every relevant jurisdiction was searched live and answered, every other source answered, and nothing reportable was found. `incomplete` means a source did not answer, a relevant jurisdiction could not be searched live, or a record could not be re-verified — read `incompleteReasons`, and treat the furnished records as a lower bound. |
| `recordCount` | `integer` |  | How many records were furnished. |
| `withheld` | `object` |  | Candidates that did not pass the controls, counted by reason code (`insufficient_identifiers`, `jurisdiction_restricted`, `time_limited`, `disputed_suppressed`, `not_current_at_source`, `verification_unavailable`, and for criminal `obsolete_non_conviction`, `obsolescence_undeterminable`). Only the counts: withheld content is never returned. `{}` when nothing was withheld. Example `{"insufficient_identifiers":2}`. |
| `createdAt` | `string (date-time)` |  | When the search was added to the order. |
| `startedAt` | `string (date-time)` or null |  | When it started running. `null` while queued. |
| `completedAt` | `string (date-time)` or null |  | When it finished. `null` until then. |
| `supersededBy` | `string` or null |  | The id of the search that replaced this one after a re-verify. `null` otherwise. |
| `coverage` | array of [CoverageRow](https://offendersearch.app/docs/fcra/results#object-coveragerow) |  | One row per source searched. `[]` until the search finishes. |
| `incompleteReasons` | `array of string` — one of `"source_unanswered"`, `"live_unavailable"`, `"verification_unavailable"` |  | Why the result is `incomplete`: `source_unanswered`, `live_unavailable` and/or `verification_unavailable`. `[]` on every other result. |
| `records` | array of [FurnishedRecord](https://offendersearch.app/docs/fcra/results#object-furnishedrecord) |  | Every furnished record. Present on `GET /v1/fcra/orders/{order_id}/searches/{search_id}` and in the report; the searches inside an order omit it. |

## FCRA search results API — GET /v1/fcra/orders/{order_id}/searches/{search_id} response fields

`GET /v1/fcra/orders/{order_id}/searches/{search_id}` · auth: `X-API-Key`

One search with its coverage and every furnished record.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `order_id` | `string` | required | The order id (`fo_…`). Example `"fo_ee0b34c5b52fd35b"`. |
| `search_id` | `string` | required | The search id (`fs_…`). Example `"fs_452884eecde9a1f2"`. |

### Response

**200** (application/json) — The search, with `records`.

The body is [the `Search` object](https://offendersearch.app/docs/fcra/results#object-search). One search in an order: its status, its result, the source-by-source coverage behind it, and the counts of records withheld by reason.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such order or search on this account. |

## Coverage — what is behind the result

Every search returns `coverage[]`: one row per source it searched, saying whether that source was searched live or in our data, when, and what it found. A `clear` is exactly the sum of these rows — read them before relying on one. Every field of a row is in the CoverageRow object below.

When the result is `incomplete`, the search’s `incompleteReasons` says why: `source_unanswered` (a source did not answer), `live_unavailable` (a relevant jurisdiction could not be searched live) and/or `verification_unavailable` (a record could not be re-verified).

## The CoverageRow object — FCRA search coverage fields

One source a search covered: whether it was searched live or in our data, when, whether it answered, and what it produced. A `clear` is exactly the sum of these rows.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders), [`GET /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders), [`GET /v1/fcra/orders/{order_id}`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders-order-id), [`POST /v1/fcra/orders/{order_id}/cancel`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders-order-id-cancel), [`GET /v1/fcra/orders/{order_id}/searches/{search_id}`](https://offendersearch.app/docs/fcra/results#reference-get-v1-fcra-orders-order-id-searches-search-id), [`POST /v1/fcra/searches/{search_id}/reverify`](https://offendersearch.app/docs/fcra/results#reference-post-v1-fcra-searches-search-id-reverify), [`GET /v1/fcra/orders/{order_id}/report`](https://offendersearch.app/docs/fcra/results#reference-get-v1-fcra-orders-order-id-report).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `source` | `string` |  | The source code. Records name their source with the same code. |
| `name` | `string` |  | The source's name. |
| `jurisdiction` | `string` |  | Where the source covers: a two-letter code, `US` for a national sex offender registry, or a comma-separated list of codes for a source that covers several. A criminal source with no listed coverage repeats its `source` code. |
| `mode` | `string` — one of `"live"`, `"snapshot"` |  | `live` — searched at the official source for this order; `snapshot` — searched in our data. |
| `checkedAt` | `string (date-time)` |  | When this source was searched for this order. |
| `dataAsOf` | `string (date-time)` or null |  | For a `snapshot` row, when our data for this source was last complete. `null` on every `live` row, and on a `snapshot` row without that date. |
| `status` | `string` — one of `"answered"`, `"unavailable"`, `"not_searched"` |  | `answered`, `unavailable` (could not be reached) or `not_searched`. |
| `relevant` | `boolean` |  | `true` when the source covers a relevant jurisdiction and so had to be searched live. |
| `restricted` | `boolean` |  | `true` when a jurisdiction rule for the order's purpose applies to this registry: its records are withheld as `jurisdiction_restricted`, it is not searched live, and it does not make the result incomplete. Always `false` on criminal rows. |
| `candidates` | `integer` |  | Possible matches this source produced, before the controls. |
| `furnished` | `integer` |  | How many of them were furnished. |
| `withheld` | `integer` |  | How many were withheld. The reasons are counted on the search's `withheld`. |
| `captureIds` | `array of string` |  | The evidence captures taken from this source for this order. `[]` for a source searched in our data. |

## Furnished records

Only records that passed every control are furnished. Each keeps its product’s standard record object — the sex offender record from the Record object reference, or the criminal record from the Criminal Search API — plus the FCRA fields defined in the FurnishedRecord object below.

A furnished exclusion-list record (`recordType: "healthcare_exclusion"`) keeps its `exclusion` block — program, authority, `convictionBased`, `excludedOn`, `reinstatedOn` — and reads `dispositionStatus: "conviction"` when the exclusion rests on a criminal conviction, or `"sanction"` when its basis is administrative; the `sanction` tier appears only when the search was placed with `exclusions: "all"`.

A furnished sex offender record

```json
{
  "recordId": "rec_a51eafcbd203558b53f5",
  "type": "sex_offender",
  "matchBasis": { "state": "dob_match", "identifiers": ["name", "dob"],
                  "detail": ["lastName:exact", "firstName:exact", "name_match"] },
  "retrievedAt": "2026-10-02T14:01:30Z",
  "verifiedAt": "2026-10-02T14:01:30Z",
  "name": { "first": "JORDAN", "middle": "", "last": "EXAMPLE", "suffix": "", "full": "JORDAN EXAMPLE" },
  "dob": "1984-02-11",
  "registrationState": "WI",
  "offenses": [{ "crime": "Sexual assault, second degree", … }],
  "source": { "jurisdiction": "WI", "registryName": "Wisconsin Sex Offender Registry", … },
  "…": "every other field of the sex offender record object"
}
```

## The FurnishedRecord object — furnished record fields

A record that passed every control. It is the product's own record object — the sex offender [Record object](https://offendersearch.app/docs/record-object) or the [criminal record object](https://offendersearch.app/docs/criminal/record-object) — with the FCRA fields below added. On a sex offender record `recordId` and `matchBasis` replace the Record object's own fields of those names.

Returned or accepted by [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders), [`GET /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders), [`GET /v1/fcra/orders/{order_id}`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders-order-id), [`POST /v1/fcra/orders/{order_id}/cancel`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders-order-id-cancel), [`GET /v1/fcra/orders/{order_id}/searches/{search_id}`](https://offendersearch.app/docs/fcra/results#reference-get-v1-fcra-orders-order-id-searches-search-id), [`POST /v1/fcra/searches/{search_id}/reverify`](https://offendersearch.app/docs/fcra/results#reference-post-v1-fcra-searches-search-id-reverify), [`GET /v1/fcra/orders/{order_id}/report`](https://offendersearch.app/docs/fcra/results#reference-get-v1-fcra-orders-order-id-report), [`GET /v1/consumer/portal/report`](https://offendersearch.app/docs/fcra/consumer-portal#reference-get-v1-consumer-portal-report).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `recordId` | `string` |  | The furnished record's id. Send it in `recordIds` to dispute the record. Example `"crim:IL-SANGAMON:2021CR004417"`. |
| `type` | `string` — one of `"sex_offender"`, `"criminal"` |  | `sex_offender` or `criminal`. |
| `matchBasis` | `object` |  | Why the record was furnished. |
| `matchBasis.state` | `string` — one of `"dob_match"` |  | Always `dob_match`: an FCRA order furnishes exact date-of-birth matches only. |
| `matchBasis.identifiers` | `array of string` |  | The identifiers that agreed: `["name", "dob"]`. |
| `matchBasis.detail` | `array of string` |  | Sex offender records only: the record's own `matchBasis` tokens from the [Record object](https://offendersearch.app/docs/record-object), such as `lastName:exact`. |
| `retrievedAt` | `string (date-time)` |  | When the record was obtained for this order. |
| `verifiedAt` | `string (date-time)` |  | When it was re-verified with the official source for this order. |
| `corrections` | `array of object` |  | Present only when a dispute resolved `modified` changed this record: each correction applied, oldest first. |
| `corrections[].disputeId` | `string` |  | The dispute that made the correction. |
| `corrections[].changes` | `object` |  | What changed, as recorded when the dispute was resolved. |
| `corrections[].correctedAt` | `string (date-time)` |  | When the correction was recorded. |
| `dispositionStatus` | `string` — one of `"conviction"`, `"non_conviction"`, `"sanction"`, `"mixed"`, `"unknown"` |  | Criminal records only: `conviction`, `non_conviction`, `sanction`, `mixed` or `unknown`, across the charges furnished. `sanction` is an exclusion-list record whose basis is not a criminal conviction — furnished only when the search asked for `exclusions: "all"`; a conviction-based exclusion reads `conviction`. |
| `cases` | `array of object` |  | Criminal records only: the [criminal record's](https://offendersearch.app/docs/criminal/record-object) own `cases`, keeping only the charges that passed the controls. Every field of a case and a charge is as the criminal record defines it, plus the one below. |
| `cases[].charges` | `array of object` |  | The furnished charges. |
| `cases[].charges[].fcraDisposition` | `string` — one of `"conviction"`, `"non_conviction"`, `"sanction"`, `"unknown"` |  | How the charge was classified for the FCRA time rules: `conviction`, `non_conviction`, `sanction` (an exclusion-list entry whose basis is not a conviction) or `unknown`. |
| `fcraRedactions` | `object` |  | Criminal records only, present when charges were removed: how many, by reason code (`obsolete_non_conviction`, `time_limited`, `obsolescence_undeterminable`). Their content is never returned. Example `{"obsolete_non_conviction":1}`. |
| `sandbox` | `boolean` |  | Present and `true` on every sandbox record, whose `recordId` begins `sandbox:`. |

## Withheld records

`withheld` counts the candidates that did not pass, by reason code (see Controls & reason codes). Their content is never returned — not in the search, the report, the webhook or the consumer portal.

## Re-verify a search

`POST /v1/fcra/searches/{searchId}/reverify` returns **202** and adds a new search of the same type to the order. The old search becomes `superseded`, with `supersededBy` pointing at the new one. The order returns to `in_progress`, and when it completes again it gets a new report, PDF and receipt. Only a finished search can be re-verified (`409 search_not_finished`), and not on a cancelled order (`409 order_cancelled`).

## Re-verify an FCRA search — POST /v1/fcra/searches/{search_id}/reverify

`POST /v1/fcra/searches/{search_id}/reverify` · auth: `X-API-Key`

Adds a new search of the same type to the order and marks this one `superseded`, with `supersededBy` pointing at the new one. The order returns to `in_progress`.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `search_id` | `string` | required | The finished search to run again (`fs_…`). Example `"fs_452884eecde9a1f2"`. |

### Response

**202** (application/json) — The new search, status `queued`.

The body is [the `Search` object](https://offendersearch.app/docs/fcra/results#object-search). One search in an order: its status, its result, the source-by-source coverage behind it, and the counts of records withheld by reason.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such search on this account. |
| 409 | `search_not_finished` | The search is not `complete`, `unavailable` or `error`. |
| 409 | `order_cancelled` | The order was cancelled. |

## The report

`GET /v1/fcra/orders/{id}/report` returns every current search (a superseded one is left out), its coverage, every furnished record and the metadata of every evidence capture in one document, as JSON or as the stored PDF. The PDF, its hashes, the evidence captures and the signed receipt are covered in Evidence & verification.

## FCRA report API — GET /v1/fcra/orders/{order_id}/report response fields

`GET /v1/fcra/orders/{order_id}/report` · auth: `X-API-Key`

The furnished report in one document: as JSON, or as the PDF rendered once when the order completed and stored, so its bytes and hash stay the same unless a re-verify completes the order again with a new report.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `order_id` | `string` | required | The order id (`fo_…`). Example `"fo_ee0b34c5b52fd35b"`. |

**Query parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `format` | `string` — one of `"json"`, `"pdf"` | optional · default `"json"` | `json` returns the report object; `pdf` returns the stored PDF with `X-Report-SHA256` and `X-PDF-SHA256` headers. |

### Response

**200** (application/json) — `format=json`: the report object below. `format=pdf`: the stored PDF bytes as `application/pdf`, with `X-Report-SHA256` (the report's `reportSha256`) and `X-PDF-SHA256` (the SHA-256 of these bytes, as signed in the receipt).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `orderId` | `string` |  | The order id. |
| `status` | `string` — one of `"complete"`, `"error"` |  | `complete`, or `error` when every search errored. |
| `createdAt` | `string (date-time)` |  | When the order was accepted. |
| `completedAt` | `string (date-time)` or null |  | When the order completed. |
| `permissiblePurpose` | `string` — one of `"employment"`, `"tenant_screening"`, `"consumer_written_instructions"`, `"legitimate_business_need"` |  | The purpose stated on the order. |
| `endUser` | `object` |  | The business the report is for. |
| `endUser.id` | `string` |  | The end user id. |
| `endUser.name` | `string` or null |  | Its legal name. |
| `clientReference` | `string` or null |  | Your reference. `null` when you sent none. |
| `subject` | `object` |  | The consumer, as sent on the order. |
| `subject.firstName` | `string` |  | First name. |
| `subject.middleName` | `string` or null |  | Middle name. `null` when none was sent. |
| `subject.lastName` | `string` |  | Last name. |
| `subject.dob` | `string (date)` |  | Full date of birth. |
| `controlVersion` | `string` |  | The controls version the order ran under. |
| `sandbox` | `boolean` |  | `true` for a sandbox order. |
| `liveScope` | `string` — one of `"relevant"`, `"all"` |  | The order's live scope. |
| `searches` | array of [Search](https://offendersearch.app/docs/fcra/results#object-search) |  | Every current search with its records. A superseded search is not in the report. |
| `evidence` | array of [Capture](https://offendersearch.app/docs/fcra/evidence#object-capture) |  | Every evidence capture behind the current searches (metadata; the bytes are one call away). |
| `integrity` | `object` |  | What the receipt signs. |
| `integrity.reportSha256` | `string` |  | SHA-256 of this report's canonical JSON — sorted keys, no whitespace — without the `integrity` block. |
| `integrity.receiptUrl` | `string` |  | The public receipt for this order: `/v1/fcra/receipts/{order_id}`. |
| `integrity.verifyUrl` | `string (uri)` |  | The page where anyone holding the PDF can check it: `https://offendersearch.app/verify/{order_id}`. |
| `integrity.note` | `string` |  | A one-sentence statement of how `reportSha256` is computed. |
| `integrity.pdfSha256` | `string` or null |  | SHA-256 of the stored PDF, the file `format=pdf` returns. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such order on this account. |
| 409 | `report_not_ready` | The order is not `complete` or `error` yet. |

---

# Evidence & verification

> Official-source captures from every live search, the stored report PDF and its hashes, the Ed25519-signed receipt, and how anyone can verify a PDF at /verify.

HTML: https://offendersearch.app/docs/fcra/evidence · Markdown: https://offendersearch.app/docs/fcra/evidence.md

## Official-source evidence

Every live check in an order — whether it finds a match or not — keeps hash-pinned evidence, so a `clear` can be shown, not only asserted. Each capture records its time, a SHA-256 hash and the bytes, and says which kind it is:

| `kind` | What it is |
| --- | --- |
| `source_response` | The official source’s own response, as received: URL, HTTP status, content type and body. Recorded wherever the live check receives the source’s response directly. |
| `parsed_result` | Our structured reading of what that source’s live search returned, as canonical JSON. Recorded for every live sex-offender check, so no check is ever without evidence. |
| `service_result` | For criminal searches: the criminal records service’s live answer for that source. |

A body over 2,000,000 bytes is stored up to that limit with `truncated: true` — its `sha256` and `size` still describe the full body — and at most 25 captures are kept per source in each search. Captures are kept for as long as the order’s evidence record.

GET /v1/fcra/orders/{id}/evidence

```json
{
  "orderId": "fo_ee0b34c5b52fd35b",
  "captures": [
    { "captureId": "fc_31f0a9c2d4e6b801", "searchId": "fs_452884eecde9a1f2", "source": "WI",
      "kind": "source_response",
      "url": "https://…official registry search page…",
      "retrievedAt": "2026-10-02T14:01:31Z", "httpStatus": 200,
      "contentType": "text/html; charset=utf-8",
      "sha256": "e3b0c442…b855", "size": 48213, "truncated": false },
    { "captureId": "fc_31f0b2e5a7c9d013", "searchId": "fs_452884eecde9a1f2", "source": "WI",
      "kind": "parsed_result", "url": null,
      "retrievedAt": "2026-10-02T14:01:31Z", "httpStatus": null,
      "contentType": "application/json",
      "sha256": "9f86d081…0f00", "size": 2210, "truncated": false }
  ]
}
```

`GET /v1/fcra/orders/{id}/evidence/{captureId}` returns one capture’s stored bytes, with its original content type and the headers `X-Capture-SHA256`, `X-Capture-Kind`, `X-Capture-Source` and `X-Capture-Truncated`. Only the account that placed the order can read them. Each search’s `coverage` rows list the `captureIds` taken from that source.

Every capture is either bytes a source returned or our labelled reading of them. Nothing in an FCRA report is a re-creation of a source’s page.

## The Capture object — FCRA evidence capture fields

One piece of official-source evidence taken during a live check: what it is, where it came from, when, and its SHA-256. The bytes are at `GET /v1/fcra/orders/{order_id}/evidence/{capture_id}`.

Returned or accepted by [`GET /v1/fcra/orders/{order_id}/report`](https://offendersearch.app/docs/fcra/results#reference-get-v1-fcra-orders-order-id-report), [`GET /v1/fcra/orders/{order_id}/evidence`](https://offendersearch.app/docs/fcra/evidence#reference-get-v1-fcra-orders-order-id-evidence).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `captureId` | `string` |  | The capture id. `GET /v1/fcra/orders/{order_id}/evidence/{capture_id}` returns its bytes. Example `"fc_461d419f538a882c"`. |
| `searchId` | `string` |  | The search it was taken for. |
| `source` | `string` |  | The source code, matching a `coverage` row. |
| `kind` | `string` — one of `"source_response"`, `"parsed_result"`, `"service_result"` |  | `source_response` — the official source's own response as received; `parsed_result` — our structured reading of that source's live search, as canonical JSON (recorded for every live sex offender check); `service_result` — for criminal searches, the criminal records service's live answer for that source. |
| `url` | `string` or null |  | The URL the source response came from. `null` on `parsed_result` and `service_result` captures. |
| `retrievedAt` | `string (date-time)` |  | When it was taken. |
| `httpStatus` | `integer` or null |  | The source's HTTP status. `null` on `parsed_result` and `service_result` captures. |
| `contentType` | `string` or null |  | The stored body's content type. |
| `sha256` | `string` |  | SHA-256 of the full body, also when the stored copy is truncated. |
| `size` | `integer` |  | Size of the full body, in bytes. |
| `truncated` | `boolean` |  | `true` when the body was over 2,000,000 bytes and the stored copy stops at that limit. At most 25 captures are kept per source per search. |

## FCRA evidence API — GET /v1/fcra/orders/{order_id}/evidence response fields

`GET /v1/fcra/orders/{order_id}/evidence` · auth: `X-API-Key`

Every official-source capture taken for the order, with its hash. Only the account that placed the order can read them.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `order_id` | `string` | required | The order id (`fo_…`). Example `"fo_ee0b34c5b52fd35b"`. |

### Response

**200** (application/json) — The capture metadata.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `orderId` | `string` |  | The order id. |
| `captures` | array of [Capture](https://offendersearch.app/docs/fcra/evidence#object-capture) |  | Every capture, superseded searches included. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such order on this account. |

## Download an FCRA evidence capture — GET /v1/fcra/orders/{order_id}/evidence/{capture_id}

`GET /v1/fcra/orders/{order_id}/evidence/{capture_id}` · auth: `X-API-Key`

One capture's stored bytes, exactly as kept, as a file download.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `order_id` | `string` | required | The order id (`fo_…`). Example `"fo_ee0b34c5b52fd35b"`. |
| `capture_id` | `string` | required | The capture's `captureId` (`fc_…`). Example `"fc_461d419f538a882c"`. |

### Response

**200** (the capture's own content type) — The stored bytes, with the headers below.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `X-Capture-SHA256` | `string (header)` |  | The capture's `sha256`: the hash of the full body. |
| `X-Capture-Kind` | `string (header)` |  | `source_response`, `parsed_result` or `service_result`. |
| `X-Capture-Source` | `string (header)` |  | The source code. |
| `X-Capture-Truncated` | `string (header)` |  | `true` when the stored copy stops at 2,000,000 bytes, otherwise `false`. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such order or capture on this account. |

## The stored PDF

`GET /v1/fcra/orders/{id}/report?format=pdf` returns the PDF rendered once, when the order completed, and stored — so its bytes, and its hash, stay the same unless a re-verify completes the order again with a new report. It lists every current search with its result and coverage, every furnished record with the name and date of birth it carries and when it was re-verified, the withheld counts, every evidence capture with its hash, and a QR code to the order’s verification page. The PDF response carries its hash in the `X-PDF-SHA256` header and the report’s in `X-Report-SHA256`. `format=json` returns the same report as JSON, with the capture metadata in `evidence[]` and an `integrity` block holding `reportSha256` (computed over the canonical JSON without that block), `pdfSha256`, `receiptUrl` and `verifyUrl`. Both are available once the order is `complete` or `error` (`409 report_not_ready` before then).

## The signed receipt

`GET /v1/fcra/receipts/{orderId}` — public, no key — returns an Ed25519-signed receipt. The signature covers the fields listed in `signedFields`: the order id, the completion time, the control version, `reportSha256` and `pdfSha256`. The receipt contains no personal data. It returns `404` for an unknown or unfinished order, and `503 receipts_unavailable` when receipt signing is unavailable.

GET /v1/fcra/receipts/{orderId}

```json
{
  "orderId": "fo_ee0b34c5b52fd35b",
  "completedAt": "2026-10-02T14:04:13Z",
  "controlVersion": "fcra-controls-2026-10-19.1",
  "reportSha256": "4b1f…9e0c",
  "pdfSha256": "a90d…71b4",
  "issuedAt": "2026-10-02T14:04:13Z",
  "algorithm": "Ed25519",
  "alg": "Ed25519",
  "keyId": "ed25519:88f16fa019c4132c",
  "signature": "Qk3x…",
  "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----\n",
  "signedFields": ["completedAt", "controlVersion", "orderId", "pdfSha256", "reportSha256"],
  "canonicalization": "Ed25519 signature (base64url, no padding) over the UTF-8 JSON of exactly the fields listed in signedFields, …"
}
```

## FCRA signed receipt API — GET /v1/fcra/receipts/{order_id}

`GET /v1/fcra/receipts/{order_id}` · no API key

Public, no API key: the Ed25519-signed receipt for a completed order. It contains no personal data.

### Request

No API key.

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `order_id` | `string` | required | The order id (`fo_…`). Example `"fo_ee0b34c5b52fd35b"`. |

### Response

**200** (application/json) — The signed receipt.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `orderId` | `string` |  | The order id. Signed. |
| `completedAt` | `string (date-time)` |  | When the order completed. Signed. |
| `controlVersion` | `string` |  | The controls version. Signed. |
| `reportSha256` | `string` |  | The report's `integrity.reportSha256`. Signed. |
| `pdfSha256` | `string` or null |  | SHA-256 of the stored PDF. Signed. |
| `issuedAt` | `string (date-time)` |  | The receipt's time, equal to `completedAt`. |
| `algorithm` | `string` — one of `"Ed25519"` |  | The signature algorithm. |
| `alg` | `string` — one of `"Ed25519"` |  | The same value as `algorithm`. |
| `keyId` | `string` |  | Identifies the signing key: `ed25519:` and 16 hex characters derived from the public key. Example `"ed25519:88f16fa019c4132c"`. |
| `signature` | `string` |  | The Ed25519 signature, base64url without padding. |
| `publicKeyPem` | `string` |  | The public key to verify `signature` with, PEM-encoded. |
| `signedFields` | `array of string` |  | The fields the signature covers, sorted. |
| `canonicalization` | `string` |  | How the signed bytes are built from `signedFields`, in one sentence. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 404 | `not_found` | Unknown order, or an order that has not completed. |
| 503 | `receipts_unavailable` | Receipt signing is unavailable. Retry. |

## Verify a PDF

Anyone holding the PDF — the end user, the consumer, an auditor — can open `https://offendersearch.app/verify/{orderId}` (the QR code on the PDF goes there), choose the file, and check it. The browser computes the file’s SHA-256 locally and compares it with the signed `pdfSha256`, and checks the signature with the published key. The file is never uploaded, and the page shows only the receipt.

### Offline

- Hash the file: `shasum -a 256 report.pdf` and compare with `pdfSha256`.
- Rebuild the signed payload: the `signedFields` and their values as compact JSON with the keys sorted, no spaces.
- Verify with OpenSSL 3: save `publicKeyPem` as `key.pem`, base64url-decode `signature` into `sig.bin`, then `openssl pkeyutl -verify -pubin -inkey key.pem -rawin -in payload.json -sigfile sig.bin`.

---

# Controls & reason codes

> Every control an FCRA order runs, in order, for sex offender and criminal searches: exact date of birth, state rules, seven-year limits and re-verification.

HTML: https://offendersearch.app/docs/fcra/controls · Markdown: https://offendersearch.app/docs/fcra/controls.md

## How the controls run

Each search in an order first runs **live** in the relevant jurisdictions, and in our data everywhere else. Live and data candidates are merged under every name supplied, then pass the controls below in order. A relevant jurisdiction that cannot be searched live makes the result `incomplete`, never `clear`.

- **Sex offender — relevant registries:** the registry of each state in `subject.addresses` and of `useLocation.state`, plus the registry of every exact-date-of-birth candidate. National registries are searched in our data unless `liveScope` is `"all"` or they produced a candidate.
- **Criminal — relevant sources:** every live-capable court and custody source covering one of those states, plus the sources of every exact-date-of-birth candidate.
- **Restricted registries are never searched live.** A registry whose records are withheld for the order’s purpose (California for every purpose; Nevada and New Jersey for tenant screening) shows `restricted: true` in `coverage` and does not make the result incomplete.
- **No addresses, no use state, no candidates:** the order has no relevant jurisdictions, its result rests on our data, and every `coverage` row says `mode: "snapshot"`. Send the address history to get live coverage.

Then each candidate is checked in order. A candidate that fails one is withheld with that reason code. The controls are versioned: the order carries `controlVersion`, and the evidence record pins a hash of the exact rules that ran.

## Sex offender search

| Step | Control | Reason code |
| --- | --- | --- |
| a | **Exact date of birth.** Only a record whose date of birth matches the subject’s exactly can be furnished. A year, age or name-only match is withheld. | `insufficient_identifiers` |
| b | **Jurisdiction and purpose restrictions.** California registry information is withheld for every purpose (Cal. Penal Code § 290.46(j)); Nevada (NRS 179B.270) and New Jersey (N.J.S.A. 2C:7-16(c)) registry information is withheld for tenant screening. The check covers each registry that holds the person. | `jurisdiction_restricted` |
| c | **Disputes.** A record removed by a resolved dispute is withheld from every later FCRA order. | `disputed_suppressed` |
| d | **Re-verified before it is furnished.** Each remaining record is re-verified with the official source. No longer listed: withheld. Source unreachable: withheld, and the result is `incomplete`. | `not_current_at_source` · `verification_unavailable` |

## Criminal search

| Step | Control | Reason code |
| --- | --- | --- |
| a | **Exact date of birth**, as above. | `insufficient_identifiers` |
| b | **Federal seven-year rule.** Charge by charge, an item that is not a conviction — an arrest, a dismissed or pending charge, a booking, a warrant — is removed once it is more than seven years old (15 U.S.C. § 1681c(a)(2), (a)(5)). Convictions are not limited by federal law. Employment at a stated `annualSalary` of $75,000 or more lifts this rule (§ 1681c(b)(3)). | `obsolete_non_conviction` |
| c | **State seven-year limits.** A conviction more than seven years old is removed when the record’s state, a state in `subject.addresses`, or `useLocation.state` is one of the states below. We apply each limit conservatively, without the exceptions some of those states allow; the salary exemption does not lift a state limit. | `time_limited` |
| — | **Missing date or disposition.** If a seven-year rule could apply but the date or disposition needed to decide it is missing, the charge is removed rather than reported on a guess. | `obsolescence_undeterminable` |
| d | **Disputes**, as above. | `disputed_suppressed` |
| e | **Re-verified before it is furnished**, and the time rules run again on the fresh copy. | `not_current_at_source` · `verification_unavailable` |

**Exclusion-list records.** A conviction-based exclusion (`exclusion.convictionBased: true`) is a conviction dated by `exclusion.excludedOn`, so steps a–e apply to it unchanged. An administrative exclusion is the `sanction` kind: reportable as a current public-record status while the exclusion is in effect (`lifecycle.droppedOut` false, no past `reinstatedOn`) and, once it has ended, a non-conviction item measured from the reinstatement date under step b. It is furnished only when the search was placed with `exclusions: "all"`; `conviction_only` (the default) withholds it, and `none` withholds every exclusion-list record. Step a holds for both tiers: a list entry with no date of birth — most SAM.gov and state entries — is withheld as `insufficient_identifiers`.

### States with a seven-year conviction limit

| State | Citation |
| --- | --- |
| CA | Cal. Civ. Code § 1786.18(a)(7) |
| CO | Colo. Rev. Stat. § 5-18-109 |
| KS | Kan. Stat. Ann. § 50-704 (citation under review) |
| MA | Mass. Gen. Laws ch. 93, § 52 (citation under review) |
| MD | Md. Code, Com. Law § 14-1203 (citation under review) |
| MT | Mont. Code Ann. § 31-3-112 |
| NH | N.H. Rev. Stat. Ann. § 359-B:5 (citation under review) |
| NM | N.M. Stat. Ann. § 56-3-6 (citation under review) |
| NY | N.Y. Gen. Bus. Law § 380-j |
| TX | Tex. Bus. & Com. Code § 20.05 |
| WA | Wash. Rev. Code § 19.182.040 (citation under review) |

## The evidence record

Every accepted order writes an evidence record (a purpose refusal is written too) holding the end user, the purpose, your consent attestation, the subject identifiers, what each source answered, the control version and hash, and every record furnished or withheld with its reason. It is written once and never edited, so the rules a past answer ran under can be shown exactly.

---

# Disputes & reinvestigation

> Open a dispute on a furnished record, track it on the 30-day clock, and receive the outcome — with corrections sent to every partner that received the record.

HTML: https://offendersearch.app/docs/fcra/disputes · Markdown: https://offendersearch.app/docs/fcra/disputes.md

## Open a dispute

When a consumer disputes a record you received from us, open a reinvestigation on the order. Name the records, the reason, the consumer’s statement, and when the dispute reached you — the 30-day clock runs from that moment (15 U.S.C. § 1681i(a)(1)); without `receivedAt` it runs from the call. Every request field is in the dispute request reference below.

POST /v1/fcra/orders/{id}/disputes

```bash
curl -X POST https://api.offendersearch.app/v1/fcra/orders/fo_ee0b34c5b52fd35b/disputes \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recordIds": ["crim:IL-SANGAMON:2021CR004417"],
    "reason": "not_me",
    "consumerStatement": "I have never lived in Sangamon County.",
    "receivedAt": "2026-10-03T09:12:00Z"
  }'
```

## FCRA dispute API — POST /v1/fcra/orders/{order_id}/disputes request and response

`POST /v1/fcra/orders/{order_id}/disputes` · auth: `X-API-Key`

Opens a reinvestigation of records furnished in this order. You receive `dispute.created`, and our team is notified.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `order_id` | `string` | required | The order id (`fo_…`). Example `"fo_ee0b34c5b52fd35b"`. |

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `recordIds` | `array of string` | required | 1 to 100 `recordId` values furnished in this order (superseded searches included). Any other id is 422 `unknown_record`. Example `["crim:IL-SANGAMON:2021CR004417"]`. |
| `reason` | `string` — one of `"not_me"`, `"inaccurate"`, `"incomplete"`, `"outdated"`, `"other"` | required | Why the consumer disputes it. |
| `consumerStatement` | `string` or null | optional | What the consumer told you, in their words, up to 8,000 characters. |
| `receivedAt` | `string (date-time)` or null | optional | When the dispute reached you, ISO 8601; `dueAt` is 30 days later. Omit it and the time of this call is used. More than five minutes in the future is a 422. |

### Response

**201** (application/json) — The dispute, status `received`, due 30 days after `receivedAt`.

The body is [the `Dispute` object](https://offendersearch.app/docs/fcra/disputes#object-dispute). A reinvestigation of furnished records, on the 30-day clock from `receivedAt`.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such order on this account. |
| 422 | `unknown_record` | A `recordIds` entry was not furnished in this order. |
| 422 | — | A field failed validation, including a `receivedAt` more than five minutes in the future (the standard validation body). |

## The dispute object

GET /v1/fcra/disputes/{id}

```json
{
  "id": "fd_acae08b7d57ccc18",
  "orderId": "fo_ee0b34c5b52fd35b",
  "origin": "partner",
  "recordIds": ["crim:IL-SANGAMON:2021CR004417"],
  "reason": "not_me",
  "consumerStatement": "I have never lived in Sangamon County.",
  "status": "investigating",
  "receivedAt": "2026-10-03T09:12:00Z",
  "dueAt": "2026-11-02T09:12:00Z",
  "extendedAt": null,
  "overdue": false,
  "notes": null,
  "changes": null,
  "resolvedAt": null
}
```

| Status | Meaning |
| --- | --- |
| `received` | Logged; the clock is running. |
| `investigating` | Our team is reinvestigating the record. |
| `verified` | The record was confirmed as furnished. Nothing changes. |
| `modified` | The record was corrected. `changes` lists what changed. |
| `deleted` | The record was removed, or found not to be the consumer’s. |
| `unable_to_verify` | It could not be verified. It is treated as deleted. |

If the consumer sends new information during the investigation, add it with `POST /v1/fcra/disputes/{id}/information` and `{ "statement": "…" }`. That is the one 15-day extension (15 U.S.C. § 1681i(a)(1)(B)): `dueAt` moves and `extendedAt` records when. It returns `409 dispute_not_extendable` once the dispute is resolved, already extended or past due. `overdue` is true for an open dispute past `dueAt`.

## The Dispute object — FCRA dispute fields

A reinvestigation of furnished records, on the 30-day clock from `receivedAt`.

Returned or accepted by [`POST /v1/fcra/orders/{order_id}/disputes`](https://offendersearch.app/docs/fcra/disputes#reference-post-v1-fcra-orders-order-id-disputes), [`GET /v1/fcra/disputes`](https://offendersearch.app/docs/fcra/disputes#reference-get-v1-fcra-disputes), [`GET /v1/fcra/disputes/{dispute_id}`](https://offendersearch.app/docs/fcra/disputes#reference-get-v1-fcra-disputes-dispute-id), [`POST /v1/fcra/disputes/{dispute_id}/information`](https://offendersearch.app/docs/fcra/disputes#reference-post-v1-fcra-disputes-dispute-id-information).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The dispute id. Example `"fd_acae08b7d57ccc18"`. |
| `orderId` | `string` |  | The order the disputed records were furnished in. |
| `origin` | `string` — one of `"partner"`, `"consumer_portal"` |  | `partner` — opened through this API; `consumer_portal` — opened by the consumer in the portal. |
| `recordIds` | `array of string` |  | The disputed `recordId` values. |
| `reason` | `string` — one of `"not_me"`, `"inaccurate"`, `"incomplete"`, `"outdated"`, `"other"` |  | The reason given. |
| `consumerStatement` | `string` or null |  | The consumer's statement. New information added with `POST /v1/fcra/disputes/{dispute_id}/information` is appended under a dated heading. `null` when none was given. |
| `receivedAt` | `string (date-time)` |  | When the dispute was received — the start of the 30-day clock. |
| `dueAt` | `string (date-time)` |  | When the reinvestigation is due: 30 days after `receivedAt`, plus 15 days once extended. |
| `extendedAt` | `string (date-time)` or null |  | When the one 15-day extension was applied. `null` until then. |
| `status` | `string` — one of `"received"`, `"investigating"`, `"verified"`, `"modified"`, `"deleted"`, `"unable_to_verify"` |  | `received` (logged; the clock is running), `investigating`, `verified` (confirmed as furnished; nothing changes), `modified` (corrected — see `changes`), `deleted` (removed, or found not to be the consumer's) or `unable_to_verify` (treated as deleted). |
| `notes` | `string` or null |  | The reinvestigation's notes. `null` until written. |
| `changes` | `object` or null |  | For a `modified` outcome, what changed. `null` otherwise. |
| `resolvedAt` | `string (date-time)` or null |  | When the outcome was reached. `null` while open. |
| `overdue` | `boolean` |  | `true` for an open dispute past `dueAt`. |

## Retrieve an FCRA dispute — GET /v1/fcra/disputes/{dispute_id}

`GET /v1/fcra/disputes/{dispute_id}` · auth: `X-API-Key`

A dispute, its due date and its outcome.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `dispute_id` | `string` | required | The dispute id (`fd_…`). Example `"fd_acae08b7d57ccc18"`. |

### Response

**200** (application/json) — The dispute.

The body is [the `Dispute` object](https://offendersearch.app/docs/fcra/disputes#object-dispute). A reinvestigation of furnished records, on the 30-day clock from `receivedAt`.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such dispute on this account. |

## Extend an FCRA dispute — POST /v1/fcra/disputes/{dispute_id}/information

`POST /v1/fcra/disputes/{dispute_id}/information` · auth: `X-API-Key`

Adds the consumer's new information to an open dispute. This is the one 15-day extension: `dueAt` moves 15 days and `extendedAt` records when.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `dispute_id` | `string` | required | The dispute id (`fd_…`). Example `"fd_acae08b7d57ccc18"`. |

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `statement` | `string` | required | The consumer's new information, 1 to 8,000 characters. |

### Response

**200** (application/json) — The extended dispute.

The body is [the `Dispute` object](https://offendersearch.app/docs/fcra/disputes#object-dispute). A reinvestigation of furnished records, on the 30-day clock from `receivedAt`.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such dispute on this account. |
| 409 | `dispute_not_extendable` | The dispute is resolved, already extended, or past `dueAt`. |

## What an outcome does

- `deleted` and `unable_to_verify` suppress the record from every later FCRA order (`disputed_suppressed`).
- `modified` attaches the correction to the record, as `corrections`, in every later order that furnishes it.
- A `record.corrected` event goes to **every partner that received the record**, not only the one that opened the dispute.

## Disputes from the consumer portal

A consumer can also dispute directly through the consumer portal. That creates the same dispute object with `origin: "consumer_portal"`, and you receive `dispute.created` for your order. A dispute that reaches us through our consumer request form, without an access code, is reinvestigated by our team and is not listed among your disputes; when it deletes or corrects a record you received, you receive `record.corrected`. `GET /v1/fcra/disputes?status=&orderId=` lists every dispute on your orders, earliest due first.

## List FCRA disputes — GET /v1/fcra/disputes

`GET /v1/fcra/disputes` · auth: `X-API-Key`

Every dispute on your orders, opened by you or in the consumer portal, earliest due first.

### Request

**Query parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `status` | `string` — one of `"received"`, `"investigating"`, `"verified"`, `"modified"`, `"deleted"`, `"unable_to_verify"` | optional | Only disputes with this status. |
| `orderId` | `string` | optional | Only disputes on this order. |

### Response

**200** (application/json) — The disputes.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `disputes` | array of [Dispute](https://offendersearch.app/docs/fcra/disputes#object-dispute) |  | The disputes, by `dueAt`. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |

---

# The consumer portal

> Every order carries an access code the consumer can use to see exactly what was furnished about them and dispute any item. How it works and what they see.

HTML: https://offendersearch.app/docs/fcra/consumer-portal · Markdown: https://offendersearch.app/docs/fcra/consumer-portal.md

## An access code on every order

Every order has `consumerPortal.url` and `consumerPortal.accessCode`. Put both in the consumer’s copy of the report or your pre-adverse notice. With the code, their last name and their date of birth, the consumer can see exactly what we furnished about them in that order and dispute any item — without contacting you first.

## What the consumer sees

- The date of the order, the screening company that placed it, the business it was for, and the stated purpose.
- Every record furnished in that order, as a plain-language summary — for a registry record, the registry, the registration status when listed, and each offense; for a criminal record, where it is held, the case number and filing date, and each charge with its disposition and date — with the name and date of birth as the record states them, and when it was re-verified. The portal API also returns the full record.
- For each search, in plain language, what was searched: the states searched live and when, any that could not be searched live, and the date the rest of the data is current to.
- A **Dispute this item** button on each record, and a tracker showing each dispute’s status, due date and outcome.

They never see withheld records, other orders, or anything not furnished in that order. The portal never asks for a Social Security number.

## Identity and protection

A view opens only when the access code, last name and date of birth all match the order; the code is accepted with or without its dashes, in any case, and the last name in any case. A mismatch returns the same generic message whatever was wrong, and five failures within an hour — for one code, or from one IP address — refuse further attempts until fewer than five fall within the last hour. A session lasts 30 minutes, and a cancelled order cannot be opened.

## Consumer portal session API — POST /v1/consumer/portal/session

`POST /v1/consumer/portal/session` · no API key

The consumer opens their file for one order with its access code, their last name and their date of birth. Returns a token valid for 30 minutes.

### Request

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `accessCode` | `string` | required | The order's `consumerPortal.accessCode`, with or without dashes, any case. Example `"K7QF-2MXP-9RDA"`. |
| `lastName` | `string` | required | The consumer's last name as on the order. Case and spacing do not matter. |
| `dob` | `string (date)` | required | The consumer's full date of birth, `YYYY-MM-DD`. |

### Response

**200** (application/json) — A session token.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `token` | `string` |  | Send as `Authorization: Bearer <token>` on the other portal routes. |
| `expiresAt` | `string (date-time)` |  | 30 minutes from now. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | `not_matched` | Any of the three did not match, or the order was cancelled. The message is the same whatever was wrong. |
| 429 | `locked` | Five failures within the last hour for this code or from this IP address. Attempts are refused until fewer than five fall within the hour. |
| 429 | `rate_limited` | Too many attempts in a short period. |
| 422 | — | A field failed validation (the standard validation body). |

## Consumer portal report API — GET /v1/consumer/portal/report response fields

`GET /v1/consumer/portal/report` · auth: `Authorization: Bearer (portal session)`

Exactly what was furnished about the consumer in this order, in plain language. Withheld records, other orders and anything not furnished are never shown.

### Request

**Headers**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | required | `Bearer <token>` from `POST /v1/consumer/portal/session`. |

### Response

**200** (application/json) — The consumer's view of the order.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `order` | `object` |  | The order, as the consumer sees it. |
| `order.id` | `string` |  | The order id. |
| `order.date` | `string (date-time)` |  | When the order was placed. |
| `order.completedAt` | `string (date-time)` or null |  | When it completed. `null` until then. |
| `order.status` | `string` — one of `"received"`, `"in_progress"`, `"partial"`, `"complete"`, `"error"` |  | The order status. |
| `order.purpose` | `string` — one of `"employment"`, `"tenant_screening"`, `"consumer_written_instructions"`, `"legitimate_business_need"` |  | The stated purpose. |
| `order.purposeLabel` | `string` |  | The purpose in plain language, for example `Employment screening`. |
| `order.requestedBy` | `string` or null |  | The screening company that placed the order. |
| `order.endUser` | `string` or null |  | The business the report was for. |
| `order.sandbox` | `boolean` |  | `true` for a sandbox order. |
| `searches` | `array of object` |  | Each current search. |
| `searches[].type` | `string` — one of `"sex_offender"`, `"criminal"` |  | `sex_offender` or `criminal`. |
| `searches[].label` | `string` |  | `Sex offender registry search` or `Criminal records search`. |
| `searches[].status` | `string` — one of `"queued"`, `"in_progress"`, `"complete"`, `"unavailable"`, `"error"` |  | The search status. |
| `searches[].result` | `string` or null — one of `"records_found"`, `"clear"`, `"incomplete"` |  | The result. `null` while the search is running. |
| `searches[].resultLabel` | `string` or null |  | The result in plain language, or `This search is still running.` |
| `searches[].completedAt` | `string (date-time)` or null |  | When the search finished. `null` while running. |
| `searches[].coverage` | `object` or null |  | What was searched, in consumer-safe form. `null` while the search is running. |
| `searches[].coverage.searchedLive` | `array of object` |  | Each source searched live, and when. |
| `searches[].coverage.searchedLive[].name` | `string` |  | The source's name. |
| `searches[].coverage.searchedLive[].jurisdiction` | `string` |  | Where it covers. |
| `searches[].coverage.searchedLive[].checkedAt` | `string (date-time)` |  | When it was searched. |
| `searches[].coverage.searchedLive[].answered` | `boolean` |  | `true` when it answered. |
| `searches[].coverage.otherSources` | `object` |  | The sources searched in our data, summarised. |
| `searches[].coverage.otherSources.count` | `integer` |  | How many answered. |
| `searches[].coverage.otherSources.oldestDataAsOf` | `string (date-time)` or null |  | The oldest `dataAsOf` among them. `null` when none carries one. |
| `searches[].coverage.otherSources.newestDataAsOf` | `string (date-time)` or null |  | The newest. |
| `records` | array of [PortalRecord](https://offendersearch.app/docs/fcra/consumer-portal#object-portalrecord) |  | Every record furnished in the order's finished searches. |
| `rights` | `object` |  | The consumer's rights. |
| `rights.summaryUrl` | `string (uri)` |  | The CFPB Summary of Your Rights Under the FCRA. |
| `rights.disputeDays` | `integer` |  | The reinvestigation period, in days: 30. |
| `rights.supportEmail` | `string` |  | Where the consumer can write to us. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | `no_session` | No `Authorization: Bearer` header. |
| 401 | `session_expired` | The token is unknown or past 30 minutes, or the order was cancelled. |

## The PortalRecord object — consumer portal record fields

One furnished record as the consumer sees it: a plain-language summary and the full record.

Returned or accepted by [`GET /v1/consumer/portal/report`](https://offendersearch.app/docs/fcra/consumer-portal#reference-get-v1-consumer-portal-report).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `recordId` | `string` |  | The record id. Send it in `recordIds` to dispute it. |
| `type` | `string` — one of `"sex_offender"`, `"criminal"` |  | `sex_offender` or `criminal`. |
| `title` | `string` |  | `Sex offender registry record` or `Criminal record`. |
| `nameOnRecord` | `string` or null |  | The name as the record states it. |
| `dobOnRecord` | `string` or null |  | The date of birth as the record states it. |
| `verifiedAt` | `string (date-time)` or null |  | When the record was re-verified with the official source. |
| `lines` | `array of string` |  | The record in plain-language lines: for a registry record, the registry, its registration status when listed, and the offenses listed; for a criminal record, where it is held and each case and charge with its disposition and date. |
| `corrected` | `boolean` |  | `true` when a dispute corrected this record. |
| `record` | [FurnishedRecord](https://offendersearch.app/docs/fcra/results#object-furnishedrecord) |  | The full furnished record. The consumer is entitled to everything in their file. |

## How portal disputes reach you

A dispute filed in the portal is the same dispute object you would open through the API, with `origin: "consumer_portal"`. You receive `dispute.created`, and every later status change as `dispute.updated`.

## Consumer portal dispute API — POST /v1/consumer/portal/disputes

`POST /v1/consumer/portal/disputes` · auth: `Authorization: Bearer (portal session)`

The consumer disputes records in this report. It creates the same dispute you would open through the API, with `origin: "consumer_portal"`, and you receive `dispute.created`.

### Request

**Headers**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | required | `Bearer <token>` from `POST /v1/consumer/portal/session`. |

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `recordIds` | `array of string` | required | 1 to 100 record ids from this report. Any other id is 422 `unknown_record`. |
| `reason` | `string` — one of `"not_me"`, `"inaccurate"`, `"incomplete"`, `"outdated"`, `"other"` | required | Why the consumer disputes it. |
| `statement` | `string` or null | optional | The consumer's statement, up to 8,000 characters. |

### Response

**201** (application/json) — The dispute.

The body is [the `PortalDispute` object](https://offendersearch.app/docs/fcra/consumer-portal#object-portaldispute). A dispute as the consumer sees it in the portal.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | `session_expired` | The session ended. Open the report again. |
| 422 | `unknown_record` | A record id is not in this report. |

## Consumer portal disputes list — GET /v1/consumer/portal/disputes

`GET /v1/consumer/portal/disputes` · auth: `Authorization: Bearer (portal session)`

Every dispute on this order — the consumer's and yours — with status, due date and outcome.

### Request

**Headers**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | required | `Bearer <token>` from `POST /v1/consumer/portal/session`. |

### Response

**200** (application/json) — The disputes.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `disputes` | array of [PortalDispute](https://offendersearch.app/docs/fcra/consumer-portal#object-portaldispute) |  | Every dispute on this order, by `dueAt`. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | `session_expired` | The session ended. Open the report again. |

## The PortalDispute object — consumer portal dispute fields

A dispute as the consumer sees it in the portal.

Returned or accepted by [`POST /v1/consumer/portal/disputes`](https://offendersearch.app/docs/fcra/consumer-portal#reference-post-v1-consumer-portal-disputes), [`GET /v1/consumer/portal/disputes`](https://offendersearch.app/docs/fcra/consumer-portal#reference-get-v1-consumer-portal-disputes).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The dispute id. |
| `recordIds` | `array of string` |  | The disputed record ids. |
| `reason` | `string` — one of `"not_me"`, `"inaccurate"`, `"incomplete"`, `"outdated"`, `"other"` |  | The reason given. |
| `consumerStatement` | `string` or null |  | The statement, or `null`. |
| `receivedAt` | `string (date-time)` |  | When it was received. |
| `dueAt` | `string (date-time)` |  | When the reinvestigation is due. |
| `extendedAt` | `string (date-time)` or null |  | When the 15-day extension was applied, or `null`. |
| `status` | `string` — one of `"received"`, `"investigating"`, `"verified"`, `"modified"`, `"deleted"`, `"unable_to_verify"` |  | The dispute status. |
| `resolvedAt` | `string (date-time)` or null |  | When the outcome was reached, or `null`. |
| `changes` | `object` or null |  | What changed on a `modified` outcome, or `null`. |

## Without an access code

A consumer without a code can still request their file or open a dispute at `/consumer`. We verify their identity before disclosing anything.

## Consumer file request and dispute API — POST /v1/consumer/requests

`POST /v1/consumer/requests` · no API key

Public: a consumer without an access code requests their file or disputes information. Identity is verified separately before anything is disclosed. It is what the `/consumer` page sends.

### Request

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `kind` | `string` — one of `"file"`, `"dispute"` | required | `file` — the consumer asks for their file; `dispute` — the consumer disputes information. A dispute is due 30 days after it is received. |
| `name` | `string` | required | The consumer's name. Text past 120 characters is cut. |
| `email` | `string` | required | An address we can reply to, up to 254 characters. It is checked for a valid shape only. |
| `message` | `string` | required | The request, at least 10 characters; text past 4,000 characters is cut. A message containing a Social Security number is refused (422 `invalid`). |
| `website` | `string` | optional | A form field the consumer page sends empty. Send it empty or omit it. |
| `elapsedMs` | `integer` | optional | Sent by the consumer page. Omit it. |

### Response

**201** (application/json) — The request, recorded.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The request id (`cons_…`). |
| `kind` | `string` — one of `"file"`, `"dispute"` |  | As sent. |
| `receivedAt` | `string (date-time)` |  | When it was received. |
| `dueAt` | `string (date-time)` or null |  | For a dispute, 30 days after `receivedAt`. `null` for a file request. |
| `supportEmail` | `string` |  | Where the consumer can write to us. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 422 | `unexpected_fields` | A field other than the six listed was sent. |
| 422 | `invalid` | A field is missing or not valid, or the message contains a Social Security number. `detail.message` lists every problem. |
| 429 | `rate_limited` | Too many requests in a short period. `Retry-After` gives the seconds to wait. |

---

# FCRA API reference

> Every FCRA API endpoint with its request and response fields, and every object it sends and returns: orders, searches, records, disputes, events.

HTML: https://offendersearch.app/docs/fcra/api-reference · Markdown: https://offendersearch.app/docs/fcra/api-reference.md

Every endpoint of the FCRA API and every object it sends and returns, in one index. Each endpoint links to its Request and Response tables — every field, its type, whether it is required, its default and an example — and each object links to the one place it is defined. Every partner call authenticates with the `X-API-Key` header on an account with FCRA Partner Access; the receipt and the consumer request are public, and the consumer portal routes take the portal session token. Refusals return `{ "detail": { "code", "message" } }` (see Errors).

## FCRA background check API endpoints

Every endpoint, with a link to its Request and Response tables. Partner routes take `X-API-Key`; the receipt and the consumer request are public; the consumer portal routes take the portal session token.

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/fcra/partner`](https://offendersearch.app/docs/fcra/access#reference-get-v1-fcra-partner) | **Partner status.** Whether this account can place FCRA orders, the purposes its agreement covers, and whether a webhook endpoint is set. Never 403: an account without access gets `enabled: false`. |
| [`POST /v1/fcra/end-users`](https://offendersearch.app/docs/fcra/access#reference-post-v1-fcra-end-users) | **Register an end user.** Registers the employer, landlord or other business a report is for, once, with the permissible purposes it holds and your certification. |
| [`GET /v1/fcra/end-users`](https://offendersearch.app/docs/fcra/access#reference-get-v1-fcra-end-users) | **List end users.** Every end user on this account. |
| [`GET /v1/fcra/end-users/{end_user_id}`](https://offendersearch.app/docs/fcra/access#reference-get-v1-fcra-end-users-end-user-id) | **Retrieve an end user.** One end user. |
| [`PATCH /v1/fcra/end-users/{end_user_id}`](https://offendersearch.app/docs/fcra/access#reference-patch-v1-fcra-end-users-end-user-id) | **Update an end user.** Changes an end user's purposes, or suspends or reinstates it. |
| [`POST /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders) | **Create an order.** Creates an order for one consumer with a sex offender search, a criminal search or both. Returns 202 with the order at `received`; the searches run in the background and report by webhook. |
| [`GET /v1/fcra/orders`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders) | **List orders.** Your orders, newest first, one page at a time. |
| [`GET /v1/fcra/orders/{order_id}`](https://offendersearch.app/docs/fcra/orders#reference-get-v1-fcra-orders-order-id) | **Retrieve an order.** The order, its status, and the status and result of each search. |
| [`POST /v1/fcra/orders/{order_id}/cancel`](https://offendersearch.app/docs/fcra/orders#reference-post-v1-fcra-orders-order-id-cancel) | **Cancel an order.** Cancels an order that has not finished. Its queued searches become `cancelled`; a search already running finishes. |
| [`GET /v1/fcra/orders/{order_id}/searches/{search_id}`](https://offendersearch.app/docs/fcra/results#reference-get-v1-fcra-orders-order-id-searches-search-id) | **Retrieve a search.** One search with its coverage and every furnished record. |
| [`POST /v1/fcra/searches/{search_id}/reverify`](https://offendersearch.app/docs/fcra/results#reference-post-v1-fcra-searches-search-id-reverify) | **Re-verify a search.** Adds a new search of the same type to the order and marks this one `superseded`, with `supersededBy` pointing at the new one. The order returns to `in_progress`. |
| [`GET /v1/fcra/orders/{order_id}/report`](https://offendersearch.app/docs/fcra/results#reference-get-v1-fcra-orders-order-id-report) | **Get the report.** The furnished report in one document: as JSON, or as the PDF rendered once when the order completed and stored, so its bytes and hash stay the same unless a re-verify completes the order again with a new report. |
| [`GET /v1/fcra/receipts/{order_id}`](https://offendersearch.app/docs/fcra/evidence#reference-get-v1-fcra-receipts-order-id) | **Get the signed receipt.** Public, no API key: the Ed25519-signed receipt for a completed order. It contains no personal data. |
| [`GET /v1/fcra/orders/{order_id}/evidence`](https://offendersearch.app/docs/fcra/evidence#reference-get-v1-fcra-orders-order-id-evidence) | **List evidence.** Every official-source capture taken for the order, with its hash. Only the account that placed the order can read them. |
| [`GET /v1/fcra/orders/{order_id}/evidence/{capture_id}`](https://offendersearch.app/docs/fcra/evidence#reference-get-v1-fcra-orders-order-id-evidence-capture-id) | **Get one capture.** One capture's stored bytes, exactly as kept, as a file download. |
| [`POST /v1/fcra/orders/{order_id}/disputes`](https://offendersearch.app/docs/fcra/disputes#reference-post-v1-fcra-orders-order-id-disputes) | **Open a dispute.** Opens a reinvestigation of records furnished in this order. You receive `dispute.created`, and our team is notified. |
| [`GET /v1/fcra/disputes`](https://offendersearch.app/docs/fcra/disputes#reference-get-v1-fcra-disputes) | **List disputes.** Every dispute on your orders, opened by you or in the consumer portal, earliest due first. |
| [`GET /v1/fcra/disputes/{dispute_id}`](https://offendersearch.app/docs/fcra/disputes#reference-get-v1-fcra-disputes-dispute-id) | **Retrieve a dispute.** A dispute, its due date and its outcome. |
| [`POST /v1/fcra/disputes/{dispute_id}/information`](https://offendersearch.app/docs/fcra/disputes#reference-post-v1-fcra-disputes-dispute-id-information) | **Add information.** Adds the consumer's new information to an open dispute. This is the one 15-day extension: `dueAt` moves 15 days and `extendedAt` records when. |
| [`GET /v1/fcra/webhook-endpoint`](https://offendersearch.app/docs/fcra/webhooks#reference-get-v1-fcra-webhook-endpoint) | **Get the webhook endpoint.** Whether a webhook endpoint is configured, and where. The secret is never returned here. |
| [`PUT /v1/fcra/webhook-endpoint`](https://offendersearch.app/docs/fcra/webhooks#reference-put-v1-fcra-webhook-endpoint) | **Set the webhook endpoint.** Sets your webhook URL. The response carries the signing `secret` when the endpoint is created and when `rotateSecret` is true — store it then. From the dashboard, only an account admin can call it. |
| [`DELETE /v1/fcra/webhook-endpoint`](https://offendersearch.app/docs/fcra/webhooks#reference-delete-v1-fcra-webhook-endpoint) | **Remove the webhook endpoint.** Removes your webhook endpoint. Events are still recorded, with delivery status `no_endpoint`, and listed by `GET /v1/fcra/events`. |
| [`GET /v1/fcra/events`](https://offendersearch.app/docs/fcra/webhooks#reference-get-v1-fcra-events) | **List events.** Every event for your account in the order it happened, each with its delivery status, so a missed delivery is never lost. |
| [`POST /v1/fcra/events/{event_id}/resend`](https://offendersearch.app/docs/fcra/webhooks#reference-post-v1-fcra-events-event-id-resend) | **Resend an event.** Delivers one event to your webhook again, now, and returns it with the result of that attempt. |
| [`POST /v1/consumer/portal/session`](https://offendersearch.app/docs/fcra/consumer-portal#reference-post-v1-consumer-portal-session) | **Open a portal session.** The consumer opens their file for one order with its access code, their last name and their date of birth. Returns a token valid for 30 minutes. |
| [`GET /v1/consumer/portal/report`](https://offendersearch.app/docs/fcra/consumer-portal#reference-get-v1-consumer-portal-report) | **Portal report.** Exactly what was furnished about the consumer in this order, in plain language. Withheld records, other orders and anything not furnished are never shown. |
| [`POST /v1/consumer/portal/disputes`](https://offendersearch.app/docs/fcra/consumer-portal#reference-post-v1-consumer-portal-disputes) | **Dispute from the portal.** The consumer disputes records in this report. It creates the same dispute you would open through the API, with `origin: "consumer_portal"`, and you receive `dispute.created`. |
| [`GET /v1/consumer/portal/disputes`](https://offendersearch.app/docs/fcra/consumer-portal#reference-get-v1-consumer-portal-disputes) | **Portal disputes.** Every dispute on this order — the consumer's and yours — with status, due date and outcome. |
| [`POST /v1/consumer/requests`](https://offendersearch.app/docs/fcra/consumer-portal#reference-post-v1-consumer-requests) | **Consumer file or dispute request.** Public: a consumer without an access code requests their file or disputes information. Identity is verified separately before anything is disclosed. It is what the `/consumer` page sends. |

## FCRA API objects — request and response fields

Every object the FCRA API sends or returns, linked to the one place its fields are defined. A furnished record is the [sex offender Record](https://offendersearch.app/docs/record-object) or the [criminal record](https://offendersearch.app/docs/criminal/record-object) with the FCRA fields added.

| Object | What it is |
| --- | --- |
| [`Address`](https://offendersearch.app/docs/fcra/access#object-address) | An end user's business address. Every field is optional; `state` is a two-letter code and is returned upper-cased. |
| [`Certification`](https://offendersearch.app/docs/fcra/access#object-certification) | The certification the end user gave you that it holds the stated purposes, recorded as you hold it. |
| [`EndUser`](https://offendersearch.app/docs/fcra/access#object-enduser) | An employer, landlord or other business your orders are for. Every order names one by `id`. |
| [`Consent`](https://offendersearch.app/docs/fcra/orders#object-consent) | Your attestation that you obtained the consumer's disclosure and authorization, recorded on the order and in its evidence record. |
| [`Subject`](https://offendersearch.app/docs/fcra/orders#object-subject) | The consumer the order is about. A full date of birth is required: the FCRA controls furnish exact date-of-birth matches only. |
| [`Alias`](https://offendersearch.app/docs/fcra/orders#object-alias) | Another name the consumer has used. Every alias is searched in addition to the subject's name. |
| [`SubjectAddress`](https://offendersearch.app/docs/fcra/orders#object-subjectaddress) | One address in the consumer's history. Each state listed here is searched live (except a registry withheld for the order's purpose) and brings its reporting limits to the criminal search. |
| [`UseLocation`](https://offendersearch.app/docs/fcra/orders#object-uselocation) | Where the job or the housing is. Its state is searched live (except a registry withheld for the order's purpose) and its reporting limits apply. |
| [`SearchSpec`](https://offendersearch.app/docs/fcra/orders#object-searchspec) | One search to run in the order. |
| [`Order`](https://offendersearch.app/docs/fcra/orders#object-order) | An FCRA order: its status, its end user and purpose, every search it carries, and the consumer portal access code. |
| [`Search`](https://offendersearch.app/docs/fcra/results#object-search) | One search in an order: its status, its result, the source-by-source coverage behind it, and the counts of records withheld by reason. |
| [`CoverageRow`](https://offendersearch.app/docs/fcra/results#object-coveragerow) | One source a search covered: whether it was searched live or in our data, when, whether it answered, and what it produced. A `clear` is exactly the sum of these rows. |
| [`FurnishedRecord`](https://offendersearch.app/docs/fcra/results#object-furnishedrecord) | A record that passed every control. It is the product's own record object — the sex offender [Record object](https://offendersearch.app/docs/record-object) or the [criminal record object](https://offendersearch.app/docs/criminal/record-object) — with the FCRA fields below added. On a sex offender record `recordId` and `matchBasis` replace the Record object's own fields of those names. |
| [`Capture`](https://offendersearch.app/docs/fcra/evidence#object-capture) | One piece of official-source evidence taken during a live check: what it is, where it came from, when, and its SHA-256. The bytes are at `GET /v1/fcra/orders/{order_id}/evidence/{capture_id}`. |
| [`Dispute`](https://offendersearch.app/docs/fcra/disputes#object-dispute) | A reinvestigation of furnished records, on the 30-day clock from `receivedAt`. |
| [`PortalRecord`](https://offendersearch.app/docs/fcra/consumer-portal#object-portalrecord) | One furnished record as the consumer sees it: a plain-language summary and the full record. |
| [`PortalDispute`](https://offendersearch.app/docs/fcra/consumer-portal#object-portaldispute) | A dispute as the consumer sees it in the portal. |
| [`WebhookEndpoint`](https://offendersearch.app/docs/fcra/webhooks#object-webhookendpoint) | Your FCRA webhook endpoint. `secret` appears only in the response that created it or rotated it. |
| [`Event`](https://offendersearch.app/docs/fcra/webhooks#object-event) | One FCRA event on your account, with its delivery status. `GET /v1/fcra/events` lists them, oldest first. |
| [`EventData`](https://offendersearch.app/docs/fcra/webhooks#object-eventdata) | The identifiers and statuses an event carries — never personal data. Which keys are present depends on the event type (see the table under Events). |
| [`WebhookDelivery`](https://offendersearch.app/docs/fcra/webhooks#object-webhookdelivery) | The JSON body POSTed to your webhook URL (or the order's `callbackUrl`), signed in `X-Offendersearch-Signature`. |
| [`Error`](https://offendersearch.app/docs/fcra/errors#object-error) | Every refusal the FCRA API raises itself. Switch on `detail.code`; show `detail.message`. A body that fails field validation returns the standard validation body instead, where `detail` is an array of `{ loc, msg, type }` entries. |

---

# Webhooks & events

> Signed webhooks for order, search and dispute events, how to verify the signature, the retry schedule, and the events list to poll when a delivery is missed.

HTML: https://offendersearch.app/docs/fcra/webhooks · Markdown: https://offendersearch.app/docs/fcra/webhooks.md

## Configuration

Set your webhook URL with `PUT /v1/fcra/webhook-endpoint` and `{ "url": "https://…" }` (from the dashboard, an account admin only). The response contains the signing `secret` — shown **only** when the endpoint is created or when you send `"rotateSecret": true`, so store it then. `GET` shows whether an endpoint is configured; `DELETE` removes it. A `callbackUrl` on an order sends that order’s events to a different URL, signed with the same secret; it needs a configured endpoint (`422 webhook_endpoint_required`), and `record.corrected` always goes to your endpoint.

## Set the FCRA webhook URL — PUT /v1/fcra/webhook-endpoint request and response

`PUT /v1/fcra/webhook-endpoint` · auth: `X-API-Key`

Sets your webhook URL. The response carries the signing `secret` when the endpoint is created and when `rotateSecret` is true — store it then. From the dashboard, only an account admin can call it.

### Request

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `url` | `string (uri)` | required | Your webhook URL, `https://`, up to 2,000 characters. Example `"https://your-cra.example/hooks/offendersearch"`. |
| `rotateSecret` | `boolean` | optional · default `false` | `true` issues a new signing secret, returned once in this response. |

### Response

**200** (application/json) — The endpoint.

The body is [the `WebhookEndpoint` object](https://offendersearch.app/docs/fcra/webhooks#object-webhookendpoint). Your FCRA webhook endpoint. `secret` appears only in the response that created it or rotated it.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 403 | — | A dashboard session of a team member who is not an admin (`detail` is a string). |
| 422 | — | `url` is not an absolute `https://` URL (the standard validation body). |

## The WebhookEndpoint object — FCRA webhook endpoint fields

Your FCRA webhook endpoint. `secret` appears only in the response that created it or rotated it.

Returned or accepted by [`GET /v1/fcra/webhook-endpoint`](https://offendersearch.app/docs/fcra/webhooks#reference-get-v1-fcra-webhook-endpoint), [`PUT /v1/fcra/webhook-endpoint`](https://offendersearch.app/docs/fcra/webhooks#reference-put-v1-fcra-webhook-endpoint).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `configured` | `boolean` |  | `true` when an endpoint is set. The other fields are present only then. |
| `url` | `string (uri)` |  | Where events are delivered. |
| `createdAt` | `string (date-time)` |  | When the endpoint was first set. |
| `updatedAt` | `string (date-time)` |  | When it last changed. |
| `signatureHeader` | `string` — one of `"X-Offendersearch-Signature"` |  | The request header that carries each delivery's signature. |
| `secret` | `string` |  | The signing secret (`whsec_…`). Present only in the `PUT` response that created the endpoint or rotated the secret; no endpoint returns it again. |

## FCRA webhook endpoint — GET /v1/fcra/webhook-endpoint

`GET /v1/fcra/webhook-endpoint` · auth: `X-API-Key`

Whether a webhook endpoint is configured, and where. The secret is never returned here.

### Request

No parameters and no body beyond the `X-API-Key` header.

### Response

**200** (application/json) — The endpoint, or `{ "configured": false }`.

The body is [the `WebhookEndpoint` object](https://offendersearch.app/docs/fcra/webhooks#object-webhookendpoint). Your FCRA webhook endpoint. `secret` appears only in the response that created it or rotated it.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |

## Remove the FCRA webhook endpoint — DELETE /v1/fcra/webhook-endpoint

`DELETE /v1/fcra/webhook-endpoint` · auth: `X-API-Key`

Removes your webhook endpoint. Events are still recorded, with delivery status `no_endpoint`, and listed by `GET /v1/fcra/events`.

### Request

No parameters and no body beyond the `X-API-Key` header.

### Response

**200** (application/json) — Whether an endpoint was removed.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `deleted` | `boolean` |  | `true` when an endpoint existed and was removed. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 403 | — | A dashboard session of a team member who is not an admin. |

## Events

| Event | When |
| --- | --- |
| `order.created` | An order was accepted. |
| `search.completed` | One search in an order finished, including one that ended in `error` — read it. |
| `order.completed` | Every search in the order finished. |
| `order.cancelled` | The order was cancelled. |
| `dispute.created` | A dispute was opened on one of your orders — by you or through the consumer portal. |
| `dispute.updated` | A dispute moved to `investigating`, was extended with new information, or was resolved. |
| `record.corrected` | A record you received was modified or removed by a dispute. |

The payload carries identifiers and statuses only — **never personal data**. Fetch the order, search or dispute with your key to read the detail.

Webhook payload

```json
{
  "id": "fev_5f1e3340306f1d02",
  "type": "search.completed",
  "createdAt": "2026-10-02T14:04:12Z",
  "data": { "orderId": "fo_ee0b34c5b52fd35b", "searchId": "fs_9c03d7e1a4b6f258", "type": "criminal",
            "status": "complete", "result": "records_found" }
}
```

## The WebhookDelivery object — FCRA webhook payload fields

The JSON body POSTed to your webhook URL (or the order's `callbackUrl`), signed in `X-Offendersearch-Signature`.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The event id. Deduplicate on it: a resend delivers the same id again. Example `"fev_5f1e3340306f1d02"`. |
| `type` | `string` — one of `"order.created"`, `"search.completed"`, `"order.completed"`, `"order.cancelled"`, `"dispute.created"`, `"dispute.updated"`, `"record.corrected"` |  | The event type, also in `X-Offendersearch-Event`. |
| `createdAt` | `string (date-time)` |  | When the event happened. |
| `data` | [EventData](https://offendersearch.app/docs/fcra/webhooks#object-eventdata) |  | Its identifiers and statuses. Fetch the order, search or dispute with your key to read the detail. |

## The EventData object — webhook event data fields

The identifiers and statuses an event carries — never personal data. Which keys are present depends on the event type (see the table under Events).

Returned or accepted by [`GET /v1/fcra/events`](https://offendersearch.app/docs/fcra/webhooks#reference-get-v1-fcra-events), [`POST /v1/fcra/events/{event_id}/resend`](https://offendersearch.app/docs/fcra/webhooks#reference-post-v1-fcra-events-event-id-resend).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `orderId` | `string` |  | The order. Present on every event except `record.corrected`. |
| `searchId` | `string` |  | `search.completed` only: the search that finished. |
| `disputeId` | `string` |  | `dispute.created`, `dispute.updated` and `record.corrected`: the dispute. |
| `recordId` | `string` |  | `record.corrected` only: the record that was corrected or removed. |
| `type` | `string` — one of `"sex_offender"`, `"criminal"` |  | `search.completed` only: the search type. |
| `status` | `string` |  | The object's status at the time: the order's (`received`, `complete`, `error`, `cancelled`), the search's (`complete`, `unavailable`, `error`) or the dispute's. Absent on `record.corrected`. |
| `result` | `string` — one of `"records_found"`, `"clear"`, `"incomplete"` |  | `search.completed` only, when the search did not error: its result. |
| `origin` | `string` — one of `"partner"`, `"consumer_portal"` |  | `dispute.created` only: where the dispute was opened. |
| `outcome` | `string` — one of `"verified"`, `"modified"`, `"deleted"`, `"unable_to_verify"` |  | `record.corrected` only: the dispute outcome that changed the record. |

## Verifying the signature

Each delivery carries `X-Offendersearch-Signature: t=<unix time>,v1=<hex HMAC-SHA256>`, computed over `t.` followed by the raw request body with your webhook secret, plus `X-Offendersearch-Event` (the type) and `X-Offendersearch-Event-Id`. Reject a delivery whose signature does not match or whose timestamp is more than five minutes old.

Verify a delivery (Node)

```javascript
import crypto from 'node:crypto';

// header: X-Offendersearch-Signature: t=1791000252,v1=5d41402a…
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
```

## Retries, resends and polling

Respond with any 2xx. Each event is delivered at once; a failed delivery is retried a minute apart for the first ten attempts, then hourly, until 24 hours after the event was created; after that its delivery status is `failed`. Each attempt waits 10 seconds for your response. `POST /v1/fcra/events/{id}/resend` delivers an event again, and `GET /v1/fcra/events?after=<eventId>&limit=` (100 by default, up to 500) returns `{ events, nextAfter }` — every event for your account in order, each with its delivery status (`pending`, `delivered`, `failed`, or `no_endpoint` when none is configured) — so a missed delivery is never lost. Events can arrive out of order; read the object’s current status rather than inferring it from the event sequence.

## FCRA events API — GET /v1/fcra/events polling response

`GET /v1/fcra/events` · auth: `X-API-Key`

Every event for your account in the order it happened, each with its delivery status, so a missed delivery is never lost.

### Request

**Query parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `after` | `string` | optional | An event id; only events after it are returned. |
| `limit` | `integer` | optional · default `100` | Events per page, 1 to 500. |

### Response

**200** (application/json) — One page of events, oldest first.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `events` | array of [Event](https://offendersearch.app/docs/fcra/webhooks#object-event) |  | The events. |
| `nextAfter` | `string` or null |  | The last event id on this page — send it as `after` for the next page. On an empty page it repeats the `after` you sent (`null` when you sent none). |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |

## The Event object — FCRA event fields

One FCRA event on your account, with its delivery status. `GET /v1/fcra/events` lists them, oldest first.

Returned or accepted by [`GET /v1/fcra/events`](https://offendersearch.app/docs/fcra/webhooks#reference-get-v1-fcra-events), [`POST /v1/fcra/events/{event_id}/resend`](https://offendersearch.app/docs/fcra/webhooks#reference-post-v1-fcra-events-event-id-resend).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The event id (`fev_…`). Also sent as `X-Offendersearch-Event-Id`. Example `"fev_5f1e3340306f1d02"`. |
| `type` | `string` — one of `"order.created"`, `"search.completed"`, `"order.completed"`, `"order.cancelled"`, `"dispute.created"`, `"dispute.updated"`, `"record.corrected"` |  | The event type. |
| `createdAt` | `string (date-time)` |  | When the event happened. |
| `data` | [EventData](https://offendersearch.app/docs/fcra/webhooks#object-eventdata) |  | Its identifiers and statuses. |
| `delivery` | `object` |  | How delivery to your webhook went. |
| `delivery.status` | `string` — one of `"pending"`, `"delivered"`, `"failed"`, `"no_endpoint"` |  | `pending` (a retry is scheduled), `delivered` (your endpoint answered 2xx), `failed` (no 2xx within 24 hours of the event) or `no_endpoint` (no webhook endpoint was configured). |
| `delivery.attempts` | `integer` |  | Delivery attempts so far. |
| `delivery.deliveredAt` | `string (date-time)` or null |  | When your endpoint answered 2xx. `null` until then. |
| `delivery.nextAttemptAt` | `string (date-time)` or null |  | When the next retry runs. `null` unless `status` is `pending`. |
| `delivery.lastStatus` | `integer` or null |  | The HTTP status of the last attempt; `0` when the connection itself failed. `null` before the first attempt. |

## Resend an FCRA webhook event — POST /v1/fcra/events/{event_id}/resend

`POST /v1/fcra/events/{event_id}/resend` · auth: `X-API-Key`

Delivers one event to your webhook again, now, and returns it with the result of that attempt.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `event_id` | `string` | required | The event id (`fev_…`). Example `"fev_5f1e3340306f1d02"`. |

### Response

**200** (application/json) — The event after the attempt.

The body is [the `Event` object](https://offendersearch.app/docs/fcra/webhooks#object-event). One FCRA event on your account, with its delivery status. `GET /v1/fcra/events` lists them, oldest first.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` (the body is `{ "detail": "…" }`, a string). |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such event on this account. |

---

# Errors

> Every error the FCRA API returns, with the stable code to switch on: access refused, purpose not allowed, validation, idempotency conflicts and cancellation.

HTML: https://offendersearch.app/docs/fcra/errors · Markdown: https://offendersearch.app/docs/fcra/errors.md

## Error codes

Every refusal the FCRA API raises itself returns `{ "detail": { "code": "…", "message": "…" } }`: switch on `detail.code`, show `detail.message`. Two refusals carry a plain string in `detail` instead: a missing or invalid key (`401`), and a dashboard team member who is not an admin changing the webhook endpoint (`403`). A body that fails field validation returns `422` with `detail` as an array of `{ loc, msg, type }` entries, one per problem.

| Status | Code | Meaning |
| --- | --- | --- |
| 401 | — | Missing or invalid `X-API-Key`. `detail` is a string. |
| 403 | `fcra_not_enabled` | This account has no active FCRA partner agreement. |
| 403 | `fcra_purpose_not_allowed` | The purpose is not in your agreement, or not held by the end user. |
| 403 | `fcra_not_on_trial` | FCRA orders are not available on a trial account. |
| 404 | `not_found` | No such end user, order, search, dispute, event or evidence capture on your account. |
| 404 | `unknown_end_user` | The `endUserId` is not one of your end users. |
| 409 | `idempotency_conflict` | The `Idempotency-Key` was used with a different body. |
| 409 | `not_cancellable` | The order is already `complete`, `error` or `cancelled`. |
| 409 | `report_not_ready` | The report and its PDF are available once the order is `complete` or `error`. |
| 409 | `search_not_finished` | Only a finished search can be re-verified. |
| 409 | `order_cancelled` | The order was cancelled. |
| 409 | `dispute_not_extendable` | The dispute is resolved, already extended, or past due. |
| 422 | — (array) | A field failed validation: a partial or future date of birth, a missing or future consent attestation, an unknown search type, purpose or `liveScope`, a repeated search type, a `callbackUrl` or webhook URL that is not `https://`, a dispute `receivedAt` in the future, or an unexpected field. `detail` lists each problem. |
| 422 | `validation` | An `Idempotency-Key` over 200 characters. |
| 422 | `end_user_suspended` | The end user is suspended. |
| 422 | `unknown_record` | A `recordIds` entry was not furnished in this order. |
| 422 | `webhook_endpoint_required` | A `callbackUrl` needs a configured webhook endpoint first. |
| 503 | `receipts_unavailable` | Receipt signing is unavailable; retry later. |
| 401 | `not_matched` | Consumer portal: the access code, last name and date of birth do not all match an order, or the order was cancelled. The message is the same whatever was wrong. |
| 401 | `no_session` · `session_expired` | Consumer portal: no session token, or the session has ended (after 30 minutes, or because the order was cancelled). |
| 429 | `locked` | Consumer portal: five failed attempts within an hour for this code or from this IP address. |
| 429 | `rate_limited` | Consumer portal or consumer request: too many requests in a short period. Retry later. |
| 422 | `unexpected_fields` · `invalid` | Consumer request: a field other than the six accepted, or a missing or invalid field (including a message with a Social Security number). |

An order refused for its purpose (`fcra_purpose_not_allowed`) is still written to the evidence record, with the reason it was refused.

`live_unavailable` is not an error: it is an `incompleteReasons` value on a completed search whose relevant jurisdiction could not be searched live. The search completes with result `incomplete`.

## The Error object — FCRA API error response fields

Every refusal the FCRA API raises itself. Switch on `detail.code`; show `detail.message`. A body that fails field validation returns the standard validation body instead, where `detail` is an array of `{ loc, msg, type }` entries.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `detail` | `object` |  | The refusal. |
| `detail.code` | `string` |  | The stable code to switch on, for example `fcra_not_enabled`. Example `"fcra_not_enabled"`. |
| `detail.message` | `string` |  | A sentence you can show. |
