# 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.

- **HTML:** https://offendersearch.app/docs/guides/fcra/creating-orders
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Docs index for agents:** https://offendersearch.app/llms.txt

## 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[].id`s (`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**
```bash
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"
  }'
```

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

BASE = "https://sandbox.offendersearch.app"
H = {"X-API-Key": os.environ["OFFENDERSEARCH_TEST_KEY"]}

res = requests.post(BASE + "/v1/fcra/orders", timeout=30,
    headers={**H, "Idempotency-Key": str(uuid.uuid4())},
    json={
        "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": None}],
        },
        "useLocation": {"state": "IL"},
        "searches": [{"type": "sex_offender"}, {"type": "criminal"}],
        "clientReference": "cra-order-889",
    })
if res.status_code != 202:
    raise RuntimeError(res.status_code, res.json())
order = res.json()
print(order["id"], order["status"], order["consumerPortal"]["accessCode"])

# poll (webhooks are the production path; the sandbox finishes right after the 202)
while order["status"] not in ("complete", "error", "cancelled"):
    time.sleep(5)
    order = requests.get(BASE + "/v1/fcra/orders/" + order["id"], headers=H, timeout=30).json()
for s in order["searches"]:
    print(s["type"], s["status"], s["result"], s["recordCount"], s["withheld"])
```

**JavaScript**
```javascript
const BASE = "https://sandbox.offendersearch.app";
const headers = { "X-API-Key": process.env.OFFENDERSEARCH_TEST_KEY, "Content-Type": "application/json" };

const res = await fetch(BASE + "/v1/fcra/orders", {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({
    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",
  }),
});
if (res.status !== 202) throw new Error(res.status + " " + (await res.text()));
let order = await res.json();
console.log(order.id, order.status, order.consumerPortal.accessCode);

// poll (webhooks are the production path; the sandbox finishes right after the 202)
while (!["complete", "error", "cancelled"].includes(order.status)) {
  await new Promise((r) => setTimeout(r, 5000));
  order = await (await fetch(BASE + "/v1/fcra/orders/" + order.id, { headers })).json();
}
for (const s of order.searches) console.log(s.type, s.status, s.result, s.recordCount, s.withheld);
```

*202 Accepted — the order at received*
```json
{
  "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

| Field | Values | What it decides |
| --- | --- | --- |
| HTTP status | `202` | Accepted. Anything else means no order exists — read `detail.code`. |
| `status` | `received` → `in_progress` → `partial` → `complete` or `error`; `cancelled` | `partial` means some searches finished and others are still running. Only `complete`, `error` and `cancelled` are final. |
| `eta.p50`, `eta.p90` | date-times or `null` | When to expect completion, from recent completions of each search type. `null` once nothing is pending. |
| `searches[].status` | `queued`, `in_progress`, `complete`, `unavailable`, `error`, `cancelled`, `superseded` | Per-search progress; `records` are not on the order object — read the search or the report. |
| `consumerPortal` | `url`, `accessCode` | What the consumer needs to see what was furnished and dispute it. |
| `Idempotent-Replayed` header | `true` | This `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.

## Related

- [Orders (reference)](https://offendersearch.app/docs/fcra/orders)
- [Results & records](https://offendersearch.app/docs/fcra/results)
- [Controls & reason codes](https://offendersearch.app/docs/fcra/controls)
- [FCRA sandbox](https://offendersearch.app/docs/fcra/sandbox)
