FCRA Partner API · v1.0.0

Creating screening orders

One consumer, one end user, one purpose, one consent attestation, one or two searches. The order is accepted at once and finishes in the background.

What you’ll build

The order call at the centre of the FCRA API: POST /v1/fcra/orders with the subject’s identifiers, the end user, the permissible purpose, your attestation of the consumer’s consent, and the searches to run (sex_offender, criminal, or both). You get a 202 with the order at status received, an ETA, and the consumer portal access code; the searches run in the background and report by webhook. On the sandbox the order finishes right after the 202, so one poll shows the complete result.

Before you start

  • A test key against https://sandbox.offendersearch.app and a registered end user (eu_…) with the purpose you will state.
  • Sandbox subjects are chosen by last name: Clear (every search clear), Records (one furnished record per search), Withheld, Unavailable, Dispute, Excluded, Sanctioned. Any other last name behaves as Clear. Use Pat Records, 1980-01-01.
  • Required: endUserId, permissiblePurpose, consent (obtainedAt, method ∈ esign, wet_signature, electronic_click, recorded_verbal, other; optional documentRef), subject (firstName, lastName, a full dob; optional middleName, up to 5 aliases, up to 20 addresses with a two-letter state), searches[] (1–2, each type at most once). Optional: useLocation.state, annualSalary, clientReference, callbackUrl, liveScope (relevant default, all), and on a criminal search exclusions (conviction_only default, all, none).
  • Send an Idempotency-Key header (≤ 200 characters). The same key and body within 24 hours returns the original order with Idempotent-Replayed: true; a different body is 409 idempotency_conflict.
The address history is what makes coverage live. Relevant jurisdictions come from subject.addresses[].state and useLocation.state (plus any exact-date-of-birth candidate). With no addresses and no use state the order has no relevant jurisdictions and its result rests on the platform’s data, every coverage row mode: "snapshot". Send the history you hold.

Step by step

  1. Assemble the order. Subject identifiers exactly as consented; the end user; the purpose that end user holds; consent.obtainedAt and method from your authorization flow; the searches; your clientReference.
  2. Send it with an Idempotency-Key. POST /v1/fcra/orders → 202. Store id (fo_…), the searches[].ids (fs_…) and consumerPortal.accessCode.
  3. Wait for the webhook, or poll. order.created, then one search.completed per search, then order.completed. Polling: GET /v1/fcra/orders/{order_id} until status is complete, error or cancelled. eta.p50/eta.p90 say when to look.
  4. Cancel if you must. POST /v1/fcra/orders/{order_id}/cancel while the order has not finished; queued searches become cancelled, a running one finishes. 409 not_cancellable once it is complete, error or cancelled.
curl -X POST https://sandbox.offendersearch.app/v1/fcra/orders \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
  -H "Idempotency-Key: 8f1c2a7e-order-889" \
  -H "Content-Type: application/json" \
  -d '{
    "endUserId": "eu_0b44acbeefeaa7d7",
    "permissiblePurpose": "employment",
    "consent": { "obtainedAt": "2026-10-25T13:55:00Z", "method": "esign", "documentRef": "auth-55120" },
    "subject": {
      "firstName": "Pat", "lastName": "Records", "dob": "1980-01-01",
      "addresses": [{ "state": "IL", "city": "Springfield", "from": "2019-01", "to": null }]
    },
    "useLocation": { "state": "IL" },
    "searches": [{ "type": "sex_offender" }, { "type": "criminal" }],
    "clientReference": "cra-order-889"
  }'
202 Accepted — the order at received
{
  "id": "fo_ee0b34c5b52fd35b",
  "status": "received",
  "createdAt": "2026-10-25T14:01:07.318402+00:00",
  "updatedAt": "2026-10-25T14:01:07.318402+00:00",
  "completedAt": null,
  "cancelledAt": null,
  "clientReference": "cra-order-889",
  "endUserId": "eu_0b44acbeefeaa7d7",
  "permissiblePurpose": "employment",
  "useLocation": { "state": "IL" },
  "controlVersion": "fcra-controls-2026-10-19.1",
  "liveScope": "relevant",
  "eta": { "p50": "2026-10-25T14:02:37+00:00", "p90": "2026-10-25T14:08:07+00:00" },
  "sandbox": true,
  "searches": [
    { "id": "fs_452884eecde9a1f2", "type": "sex_offender", "status": "queued", "result": null,
      "recordCount": 0, "withheld": {}, "createdAt": "2026-10-25T14:01:07.318402+00:00",
      "startedAt": null, "completedAt": null, "supersededBy": null, "coverage": [], "incompleteReasons": [] },
    { "id": "fs_9c03d7e1a4b6f258", "type": "criminal", "status": "queued", "result": null,
      "recordCount": 0, "withheld": {}, "createdAt": "2026-10-25T14:01:07.318402+00:00",
      "startedAt": null, "completedAt": null, "supersededBy": null, "coverage": [], "incompleteReasons": [] }
  ],
  "consumerPortal": { "url": "https://offendersearch.app/consumer/report", "accessCode": "K7QF-2MXP-9RDA" }
}

Three things to store from the 202: the order id, each search id, and consumerPortal.accessCode — the code is shown only to the ordering account and belongs in the consumer’s copy of the report. controlVersion names the rule set this order runs under.

Reading the result

FieldValuesWhat it decides
HTTP status202Accepted. Anything else means no order exists — read detail.code.
statusreceived → in_progress → partial → complete or error; cancelledpartial means some searches finished and others are still running. Only complete, error and cancelled are final.
eta.p50, eta.p90date-times or nullWhen to expect completion, from recent completions of each search type. null once nothing is pending.
searches[].statusqueued, in_progress, complete, unavailable, error, cancelled, supersededPer-search progress; records are not on the order object — read the search or the report.
consumerPortalurl, accessCodeWhat the consumer needs to see what was furnished and dispute it.
Idempotent-Replayed headertrueThis 202 is the original order for a repeated key, not a new order.

Edge cases and pitfalls

  • `404 unknown_end_user` — the endUserId is not one of yours; `422 end_user_suspended` — it is, but suspended.
  • `403 fcra_purpose_not_allowed` — the purpose is not held by the end user or not covered by your agreement. This refusal is written to the evidence record with its reason.
  • `422 webhook_endpoint_required` — you sent a callbackUrl without a configured webhook endpoint; the endpoint’s secret is what signs events sent to the callback.
  • `422` array — a partial or future dob, a consent.obtainedAt in the future, the same search type twice, a callbackUrl that is not https:// on a public host, an unknown field. Each entry has loc, msg, type.
  • `409 idempotency_conflict` — the key was reused with a different body within 24 hours. Mint a new key per distinct order; reuse a key only to retry the same body.
  • A running search is not cancelled by /cancel; it finishes and is furnished. Cancel early or not at all.
  • Sandbox orders finish immediately; production orders take the ETA. Build the webhook path first and treat polling as the fallback.

Production checklist

  • Always send Idempotency-Key; retry a timeout with the same key and body.
  • Send the address history and useLocation.state; send annualSalary when the purpose is employment and you hold it.
  • Persist fo_, fs_ ids and the access code; put the code and portal URL in the consumer’s copy of the report.
  • Consume order.completed and search.completed webhooks; poll only as a fallback.
  • The consent attestation records what you obtained and how; obtaining disclosure, authorization and consent from the consumer stays with you.

Product overview