# Order statuses and retrieving results

> A search ends in one of three results, and every record it did not furnish has a reason code. Read the status, then the result, then the withheld counts.

- **HTML:** https://offendersearch.app/docs/guides/fcra/order-statuses-and-results
- **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 result reader: code that walks an order’s searches, classifies each as `clear`, `records_found` or `incomplete`, reads the furnished records from `GET /v1/fcra/orders/{order_id}/searches/{search_id}`, and explains every withheld record by its reason code. You will use the sandbox subjects **Records**, **Withheld** and **Unavailable** to see all three outcomes.

Furnished records are the product’s own record object plus FCRA fields; the controls that decide furnishing versus withholding are documented in full in [Controls & reason codes](/docs/fcra/controls) — this guide shows you how to read their output.

## Before you start

- Three completed sandbox orders (see Creating screening orders): **Pat Records** with both searches, **Pat Withheld** with a criminal search, **Pat Unavailable** with a sex-offender search.
- Records appear only on `GET /v1/fcra/orders/{order_id}/searches/{search_id}` and in the report — never on the order object.
- Order statuses: `received`, `in_progress`, `partial`, `complete`, `cancelled`, `error`. Search statuses: `queued`, `in_progress`, `complete`, `unavailable`, `error`, `cancelled`, `superseded`. Search results: `clear`, `records_found`, `incomplete` (`null` until finished and on `error`).

> **`clear` with withheld counts is still `clear`.** A search that found candidates and withheld all of them — because their date of birth did not match exactly, or they were no longer current at the source — reports `result: "clear"`, `recordCount: 0` and non-empty `withheld` counts. The counts explain; they are not records and are never shown to the consumer.

## Step by step

1. **Read the order.** `GET /v1/fcra/orders/{order_id}`. Stop when `status` is `complete`, `error` or `cancelled`; `partial` means keep waiting.
2. **Classify each search.** `status` first: `complete` and `unavailable` are finished; `error` has no result. Then `result`: `records_found` → read records; `clear` → check `withheld`; `incomplete` → read `incompleteReasons` and `coverage`.
3. **Read the records.** `GET /v1/fcra/orders/{order_id}/searches/{search_id}` returns `records[]`: each is the product record plus `recordId`, `type`, `matchBasis`, `retrievedAt`, `verifiedAt`, and on criminal records `dispositionStatus`, per-charge `fcraDisposition` and `fcraRedactions`.
4. **Read the coverage.** `coverage[]` has one row per source: `mode` (`live`/`snapshot`), `status` (`answered`/`unavailable`/`not_searched`), `relevant`, `restricted`, `candidates`, `furnished`, `withheld`, `captureIds[]`.

**cURL**
```bash
curl https://sandbox.offendersearch.app/v1/fcra/orders/fo_ee0b34c5b52fd35b/searches/fs_9c03d7e1a4b6f258 \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY"
```

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

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

def read_order(order_id):
    order = requests.get(BASE + "/v1/fcra/orders/" + order_id, headers=H, timeout=30).json()
    if order["status"] not in ("complete", "error", "cancelled"):
        return "pending", order
    outcome = {}
    for s in order["searches"]:
        if s["status"] == "superseded":
            continue                                   # replaced by a re-verify; read the new one
        if s["status"] == "error":
            outcome[s["type"]] = ("error", [], s["withheld"])
            continue
        detail = requests.get(BASE + "/v1/fcra/orders/" + order_id + "/searches/" + s["id"],
                              headers=H, timeout=30).json()
        outcome[s["type"]] = (detail["result"], detail["records"], detail["withheld"],
                              detail["incompleteReasons"])
    return order["status"], outcome

status, outcome = read_order("fo_ee0b34c5b52fd35b")
for kind, (result, records, withheld, *rest) in outcome.items():
    print(kind, result, len(records), "furnished;", withheld, "withheld", rest)
```

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

async function readOrder(orderId) {
  const order = await (await fetch(BASE + "/v1/fcra/orders/" + orderId, { headers })).json();
  if (!["complete", "error", "cancelled"].includes(order.status)) return ["pending", order];
  const outcome = {};
  for (const s of order.searches) {
    if (s.status === "superseded") continue;             // replaced by a re-verify; read the new one
    if (s.status === "error") { outcome[s.type] = { result: "error", records: [], withheld: s.withheld }; continue; }
    const d = await (await fetch(BASE + "/v1/fcra/orders/" + orderId + "/searches/" + s.id, { headers })).json();
    outcome[s.type] = { result: d.result, records: d.records, withheld: d.withheld, incompleteReasons: d.incompleteReasons };
  }
  return [order.status, outcome];
}

const [status, outcome] = await readOrder("fo_ee0b34c5b52fd35b");
for (const [kind, o] of Object.entries(outcome)) {
  console.log(kind, o.result, o.records.length, "furnished;", o.withheld, "withheld", o.incompleteReasons);
}
```

*200 — the criminal search for Pat Records*
```json
{
  "id": "fs_9c03d7e1a4b6f258",
  "type": "criminal",
  "status": "complete",
  "result": "records_found",
  "recordCount": 1,
  "withheld": {},
  "createdAt": "2026-10-25T14:01:07.318402+00:00",
  "startedAt": "2026-10-25T14:01:07.402118+00:00",
  "completedAt": "2026-10-25T14:01:07.455930+00:00",
  "supersededBy": null,
  "coverage": [],
  "incompleteReasons": [],
  "records": [
    {
      "recordId": "sandbox:criminal:records-1980-01-01",
      "type": "criminal",
      "matchBasis": { "state": "dob_match", "identifiers": ["name", "dob"] },
      "retrievedAt": "2026-10-25T14:01:07.455930+00:00",
      "verifiedAt": "2026-10-25T14:01:07.455930+00:00",
      "sandbox": true,
      "name": { "first": "Pat", "last": "Records", "full": "Pat Records" },
      "dob": "1980-01-01",
      "sources": [{ "code": "ZZ-SANDBOX", "level": "county", "live": true }],
      "state": "ZZ",
      "dispositionStatus": "conviction",
      "cases": [
        { "caseNumber": "SBX-0001", "court": "Sandbox County Court", "filedDate": "2021-03-04",
          "charges": [
            { "description": "Sandbox charge (test data)", "level": "misdemeanor", "disposition": "Guilty",
              "dispositionDate": "2021-09-10", "fcraDisposition": "conviction" }
          ] }
      ]
    }
  ]
}
```

*Pat Withheld — clear, with the reasons*
```json
{
  "id": "fs_1e7b4c9a3d5f2086",
  "type": "criminal",
  "status": "complete",
  "result": "clear",
  "recordCount": 0,
  "withheld": { "insufficient_identifiers": 2, "not_current_at_source": 1 },
  "incompleteReasons": [],
  "records": []
}
```

*Pat Unavailable — the sources could not be reached*
```json
{
  "id": "fs_7a2d9e4c1b6f3058",
  "type": "sex_offender",
  "status": "unavailable",
  "result": "incomplete",
  "recordCount": 0,
  "withheld": {},
  "incompleteReasons": ["live_unavailable"],
  "records": []
}
```

Sandbox searches have empty `coverage` because they make no live calls; a production search lists every source consulted. The production coverage shape is in [Results & records](/docs/fcra/results).

## Reading the result

| Field | Values | What it decides |
| --- | --- | --- |
| `result` | `records_found` | At least one record passed every control and was re-verified. Read `records[]`. |
| `result` | `clear` | Nothing was furnished. `withheld` may be non-empty — candidates existed but failed a control. |
| `result` | `incomplete` | A relevant source could not be searched live or a record could not be re-verified; never `clear`. `incompleteReasons` ⊆ `source_unanswered`, `live_unavailable`, `verification_unavailable`. |
| `withheld` | reason → count | `insufficient_identifiers` (no exact date-of-birth match), `jurisdiction_restricted`, `disputed_suppressed`, `not_current_at_source`, `verification_unavailable`; criminal adds `obsolete_non_conviction`, `obsolescence_undeterminable`, `time_limited`. |
| `records[].matchBasis.state` | `dob_match` | Always: only exact-date-of-birth records are furnished. |
| `records[].dispositionStatus` | `conviction`, `non_conviction`, `sanction`, `mixed`, `unknown` | Criminal only, across furnished charges. `sanction` is an administrative exclusion-list record, furnished only with `exclusions: "all"`. |
| `records[].fcraRedactions` | reason → count | Charges removed from a furnished record by the time rules. |
| `controlVersion` (order) | `fcra-controls-2026-10-19.1` | The rule set the order ran under; the evidence record pins a hash of the exact rules. |

## Edge cases and pitfalls

- **`partial` is not final.** One search finished, another is running. Keep waiting for `order.completed`.
- **`status: "unavailable"` is finished.** Its `result` is `incomplete`; the order still completes. Decide what an incomplete check means for your workflow before go-live.
- **`error` has `result: null`** and is reported by `search.completed` with `status: "error"` and no `result`. Read it; do not infer `clear`.
- **Superseded searches** (after a re-verify) stay on the order with `status: "superseded"` and `supersededBy`; skip them and read the new search.
- **Exclusion-list records** are criminal records with `recordType: "healthcare_exclusion"`; a conviction-based one reads `dispositionStatus: "conviction"`, an administrative one `"sanction"` and appears only under `exclusions: "all"`. A list entry with no date of birth is withheld as `insufficient_identifiers`.
- **`corrections[]`** appears on a record a resolved dispute modified; show it with the record.
- **`restricted: true`** in a coverage row means a registry whose records are withheld for the order’s purpose; it was not searched live and does not make the result incomplete.

## Production checklist

- Branch on `status` before `result`; treat `incomplete` and `error` as their own outcomes, never as `clear`.
- Show reviewers the `withheld` reasons and `fcraRedactions`; show the consumer only furnished records (the portal does this).
- Store `controlVersion` with every order you adjudicate on.
- Skip `superseded` searches; read `supersededBy`.
- Adjudication and any notices to the consumer stay with you; the API furnishes records and the reasons it did not.

## Related

- [Results & records (reference)](https://offendersearch.app/docs/fcra/results)
- [Controls & reason codes](https://offendersearch.app/docs/fcra/controls)
- [FCRA enums](https://offendersearch.app/docs/fcra/enums)
- [Criminal record object](https://offendersearch.app/docs/criminal/record-object)
