# Background check API for HR systems and ATS integration

> For an engineer at an HR platform or applicant tracking system: where the check sits in the hiring flow, why it must be an FCRA order rather than a standard search, and the exact calls from offer to result.

- **HTML:** https://offendersearch.app/docs/learn/background-check-api-for-hr-ats
- **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

## Hiring is an FCRA decision: which background check API path an HR system uses

> **Read this before you write a line of code.** A check used to decide whether to hire someone is a consumer report. Every response from the standard [background check API](/background-check-api) carries `legal.notice` "Not a consumer report. This information may not be used for any purpose under the Fair Credit Reporting Act." and **must not** be used for the hiring decision. The regulated path is an FCRA order through [FCRA Partner Access](/fcra), for approved partners, with consent, the permissible purpose `employment`, the adverse-action endpoints and the consumer portal. This page describes what the endpoints do; what your product is obliged to do is for counsel.

An applicant tracking system is the textbook case, because the decision and the data sit in the same screen. The standard `POST /v1/search` and `POST /v1/criminal/search` answer in seconds with a match score, which is why an engineer reaches for them and why they are wrong here: no consent, no permissible purpose, no adverse-action process, no consumer portal. The FCRA order has more fields because each one is a legal requirement rendered as JSON. The [FCRA employment screening](/fcra-employment-screening) page and [how it works](/docs/fcra/how-it-works) describe the partner relationship; the [FCRA API](/fcra-api) page is the product summary.

| You want to… | Endpoint | Why |
| --- | --- | --- |
| Decide whether to hire, promote or retain | `POST /v1/fcra/orders` | A consumer report: consent, permissible purpose, adverse action, consumer portal. |
| Run a lookup that drives no employment decision | `POST /v1/search`, `POST /v1/criminal/search` | Standard API; `legal.notice` applies. |
| Be told when a watched person appears on a registry | `POST /v1/monitors` | Monitoring is not a consumer report; acting on an alert for employment is an FCRA workflow. |

## Where the background check call sits in an ATS

In most hiring flows the check belongs at the **offer stage**: an offer is extended conditional on the check, the candidate authorises it, the order is created, and the result lands on the candidate record. Running it earlier has legal weight (some jurisdictions restrict when criminal history may be asked about), so make the stage configurable per employer and never run anything before consent exists.

Three objects in your data model map onto the API. The **employer** is an FCRA *end user*, registered once with `POST /v1/fcra/end-users`; its `id` (`eu_…`) goes on every order it places. The **requisition** is what an *order* hangs off: store the order `id` (`fo_…`) and your `clientReference` on it. The **candidate** is the order's *subject*; the consumer portal access code is for them, not the recruiter. The [end users guide](/docs/guides/fcra/end-users) describes the employer's certification.

*Register the employer once*
```bash
# Once per hiring employer on your platform, not once per candidate
curl -X POST https://sandbox.offendersearch.app/v1/fcra/end-users \
  -H "X-API-Key: $OFFENDERSEARCH_API_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-11-03T14:00:00Z",
      "certifiedBy": "compliance@ats.example.com"
    }
  }'
# → { "id": "eu_…", … }   store it on the employer record
```

A staffing platform registers each client it places candidates with as its own end user; a single-employer HR system registers one. See [employment screening API](/employment-screening-api).

## Create the FCRA background check order from the ATS

The order body carries the employer, the purpose, the consent, the candidate, where the job is, and which searches to run. `consent` records when and how the candidate authorised the check and a document reference; `subject` takes the legal name, date of birth, and any `aliases` and `addresses` supplied; `useLocation.state` is where the job is and drives the adverse-action rule; `searches` lists what to run, here the registry search and a criminal search with healthcare `exclusions`; `callbackUrl` is where the order's events go; and the `Idempotency-Key` header makes a retried submission return the same order.

**curl**
```bash
curl -X POST https://sandbox.offendersearch.app/v1/fcra/orders \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" \
  -H "Idempotency-Key: ats-req-100482" \
  -H "Content-Type: application/json" \
  -d '{
    "endUserId": "eu_0b44acbeefeaa7d7",
    "permissiblePurpose": "employment",
    "consent": { "obtainedAt": "2026-11-03T13:55:00Z", "method": "esign", "documentRef": "auth-55120" },
    "subject": {
      "firstName": "Pat", "lastName": "Records", "dob": "1980-01-01",
      "aliases": [{ "firstName": "Pat", "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": "ats-req-100482",
    "callbackUrl": "https://ats.example.com/hooks/offendersearch"
  }'
```

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

HOST = "https://sandbox.offendersearch.app"            # api.offendersearch.app with an os_live_ key
KEY = os.environ["OFFENDERSEARCH_API_KEY"]             # os_test_…

def create_order(requisition_id: str, candidate: dict, end_user_id: str, consent: dict) -> dict:
    body = {
        "endUserId": end_user_id,                      # the hiring employer, registered once
        "permissiblePurpose": "employment",
        "consent": consent,                            # {"obtainedAt", "method", "documentRef"}
        "subject": {
            "firstName": candidate["firstName"],
            "lastName": candidate["lastName"],
            "dob": candidate["dob"],
            "aliases": candidate.get("aliases", []),
            "addresses": candidate.get("addresses", []),
        },
        "useLocation": {"state": candidate["workState"]},
        "liveScope": "relevant",
        "searches": [{"type": "sex_offender"}, {"type": "criminal", "exclusions": "all"}],
        "clientReference": requisition_id,
        "callbackUrl": "https://ats.example.com/hooks/offendersearch",
    }
    r = requests.post(f"{HOST}/v1/fcra/orders", json=body, timeout=30,
                      headers={"X-API-Key": KEY, "Idempotency-Key": f"ats-{requisition_id}"})
    r.raise_for_status()                               # 202: the order is "received"
    return r.json()

order = create_order(
    "req-100482",
    {"firstName": "Pat", "lastName": "Records", "dob": "1980-01-01", "workState": "IL"},
    "eu_0b44acbeefeaa7d7",
    {"obtainedAt": "2026-11-03T13:55:00Z", "method": "esign", "documentRef": "auth-55120"},
)
print(order["id"], order["status"])                   # fo_… received
# Store order["id"] and order["consumerPortal"] on the candidate record now.
```

**Node**
```javascript
// Node 18+ (global fetch)
const HOST = "https://sandbox.offendersearch.app";     // api.offendersearch.app with an os_live_ key
const KEY = process.env.OFFENDERSEARCH_API_KEY;        // os_test_…

async function createOrder(requisitionId, candidate, endUserId, consent) {
  const body = {
    endUserId,                                         // the hiring employer, registered once
    permissiblePurpose: "employment",
    consent,                                           // { obtainedAt, method, documentRef }
    subject: {
      firstName: candidate.firstName,
      lastName: candidate.lastName,
      dob: candidate.dob,
      aliases: candidate.aliases ?? [],
      addresses: candidate.addresses ?? [],
    },
    useLocation: { state: candidate.workState },
    liveScope: "relevant",
    searches: [{ type: "sex_offender" }, { type: "criminal", exclusions: "all" }],
    clientReference: requisitionId,
    callbackUrl: "https://ats.example.com/hooks/offendersearch",
  };
  const res = await fetch(HOST + "/v1/fcra/orders", {
    method: "POST",
    headers: { "X-API-Key": KEY, "Idempotency-Key": "ats-" + requisitionId, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (res.status !== 202) throw new Error("HTTP " + res.status + " " + (await res.text()));
  return res.json();                                   // the order, status "received"
}

const order = await createOrder(
  "req-100482",
  { firstName: "Pat", lastName: "Records", dob: "1980-01-01", workState: "IL" },
  "eu_0b44acbeefeaa7d7",
  { obtainedAt: "2026-11-03T13:55:00Z", method: "esign", documentRef: "auth-55120" },
);
console.log(order.id, order.status);                  // fo_… received
// Store order.id and order.consumerPortal on the candidate record now.
```

The answer is `202` with the order at `status` `received`; the searches run in the background. Store `id`, `status` and `consumerPortal` on the candidate record immediately. If the ATS collects the authorisation, `consent.method` and `documentRef` point at your record of it; otherwise [hosted consent](/docs/fcra/hosted-consent) issues an invitation the candidate completes. The full body, `liveScope` and the search types are on the [orders reference](/docs/fcra/orders) and the [creating orders guide](/docs/guides/fcra/creating-orders).

## Wait for order.completed: webhook or polling from the HR system

An order takes minutes, so the ATS should not hold a request open. Register one webhook endpoint with `PUT /v1/fcra/webhook-endpoint`; the response carries the signing secret once. Events carry ids and statuses only, never a record: the event says *that* the order is done; you fetch the order to learn *what* it found.

*Register, receive, or poll*
```bash
# Register your receiver once; the secret comes back once
curl -X PUT https://sandbox.offendersearch.app/v1/fcra/webhook-endpoint \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://ats.example.com/hooks/offendersearch"}'

# What arrives when the order finishes (ids and statuses only):
{ "id": "fev_…", "type": "order.completed", "createdAt": "…",
  "data": { "orderId": "fo_…", "status": "complete" } }

# Or poll — the same object the webhook tells you to fetch
curl https://sandbox.offendersearch.app/v1/fcra/orders/fo_… -H "X-API-Key: $OFFENDERSEARCH_API_KEY"
```

Polling works too: `GET /v1/fcra/orders/{order_id}` until `status` is `complete` (via `received`, `in_progress`, sometimes `partial`), paced by the order's `eta` (`p50`, `p90`). Either way, deduplicate on the event `id`: deliveries are at-least-once. The receiver code is on the [webhook example](/docs/learn/webhook-example) page; `GET /v1/fcra/events` lists every event with its delivery state, and the [FCRA webhooks reference](/docs/fcra/webhooks) is the catalogue.

## Read the background check result onto the candidate record

A completed order has one entry per search in `searches[]`, each with its own `status`, `result` and `recordCount`:

*GET /v1/fcra/orders/{order_id} (abridged)*
```json
{
  "id": "fo_ee0b34c5b52fd35b",
  "status": "complete",
  "clientReference": "ats-req-100482",
  "endUserId": "eu_0b44acbeefeaa7d7",
  "permissiblePurpose": "employment",
  "useLocation": { "state": "IL" },
  "searches": [
    { "id": "fs_452884eecde9a1f2", "type": "sex_offender", "status": "complete", "result": "clear",
      "recordCount": 0, "withheld": { "insufficient_identifiers": 2 }, "incompleteReasons": [] /* … */ },
    { "id": "fs_9c03d7e1a4b6f258", "type": "criminal", "status": "complete", "result": "records_found",
      "recordCount": 1, "withheld": { "insufficient_identifiers": 1 }, "incompleteReasons": [] /* … */ }
  ],
  "consumerPortal": { "url": "https://offendersearch.app/consumer/report", "accessCode": "K7QF-2MXP-9RDA" }
  /* … */
}

# One search's records:  GET /v1/fcra/orders/fo_…/searches/fs_…
# The report:            GET /v1/fcra/orders/fo_…/report
```

- **`result: "clear"`** — no record furnished. `withheld` may still count candidates the furnishing rules kept out (`insufficient_identifiers`, `not_current_at_source`); they are not a finding.
- **`result: "records_found"`** — one or more records are furnished; `recordCount` says how many, and `GET /v1/fcra/orders/{order_id}/searches/{search_id}` returns them.
- **`result: "incomplete"`** — the search could not finish; `incompleteReasons[]` says why. Do not present it as clear.

Store the per-search `status`, `result` and `recordCount` on the candidate record, and link the report (`GET /v1/fcra/orders/{order_id}/report`) for the users entitled to open it. Show the recruiter the result; show the candidate the consumer portal: `consumerPortal.url` and `accessCode` let the candidate read the same report and start a dispute. See [consumer portal](/docs/fcra/consumer-portal) and [results](/docs/fcra/results). A record on the report is not a verdict; whether it matters for the job is the adjudication, which the next section turns into endpoints.

## Adverse action from the ATS: the two-step notice flow

When a recruiter intends to withdraw an offer because of the report, the ATS must not let that happen as a single click. `GET /v1/fcra/orders/{order_id}/adverse-items` lists every record the current report furnishes with the use location's rule; the recruiter selects the items the decision rests on, and the ATS calls `POST /v1/fcra/orders/{order_id}/adverse-actions` with those `adverseItemIds`, optional `reasonText`, and `waitingDays` (7–60; the platform applies the longest of that number, the use location's rule and a five-business-day floor). That sends the pre-adverse notice now, schedules the post-adverse notice for after the waiting period, and answers `201` with an adverse action `id` (`fa_…`), `status`, `preNoticeSentAt` and `postNoticeScheduledAt`.

During the waiting period the candidate may dispute through the consumer portal; `POST /v1/fcra/adverse-actions/{aa_id}/pause` holds the clock, `/resume` restarts it, `/cancel` withdraws the action. `POST /v1/fcra/orders/{order_id}/adjudication` with `{ "decision": "engaged" | "cleared" }` records the outcome. Model this as a state on the candidate record driven by the `adverse_action.*` webhook events. The [FCRA adverse action API tutorial](/docs/learn/fcra-adverse-action-api) walks through the calls with bodies, the [adverse action reference](/docs/fcra/adverse-action) lists the error codes (`order_not_complete`, `individualized_assessment_required`, `waiting_period_too_short`), and the [adverse action process](/adverse-action-process) page is the plain-language overview. Whether the process satisfies the law in a jurisdiction is for counsel.

## What the standard background check API is good for in an HR product

The standard API still has a place in an HR product, as long as the place is not the hiring decision:

- **A trust-and-safety or research lookup** that no employment decision depends on. `POST /v1/search` with name and date of birth answers in seconds; the [registry tutorial](/docs/learn/check-registered-sex-offender-api) is the walkthrough.
- **Re-screen alerting with monitors.** `POST /v1/monitors` with `product`, `type: "person"`, the person's name and date of birth and a `channels.webhookUrl` fires a signed `monitor.alert` when a watched person appears, changes or is removed. Monitoring is not a consumer report; if an alert leads an employer to act on someone's employment, that action is an FCRA workflow and belongs in an order (see [continuous checks](/docs/fcra/continuous-checks)). The [monitoring tutorial](/docs/learn/monitor-a-person-with-webhooks) builds the receiver.
- **Developer tooling and demos** against the [sandbox](/sandbox), where no real person and no real decision is involved.

Standard API pricing is $0.15 a call, $0.11 past 2,000 calls in a month, 25 free calls, no card; monitors are $3 a month per person monitor, $10 per location monitor. FCRA orders are priced in the partner agreement. The [employment screening API](/employment-screening-api) page is the product summary for this audience; [which API](/docs/products/which-api) is the general decision guide.

## Test the ATS background check integration in the sandbox

The sandbox runs the whole FCRA flow with a free `os_test_` key, so you can build end to end before the partner application is approved. Three subject names produce the three outcomes an ATS must handle; any other last name behaves like the first.

| Subject | Scenario | What the order returns |
| --- | --- | --- |
| Pat Clear | `fcra-clear` | Order `complete`; every search `complete` with `result` `clear` and `recordCount` 0. |
| Pat Records, 1980-01-01 | `fcra-records` | Order `complete`; each search `records_found` with one sandbox record (the criminal one carries `cases[]` with a misdemeanor conviction). |
| Pat Withheld, 1980-01-01 | `fcra-withheld` | Criminal search `complete` with `result` `clear`, `recordCount` 0 and `withheld` `{ "insufficient_identifiers": 2, "not_current_at_source": 1 }`: nothing furnished, only withheld counts. |

Run the three through your ATS, then rehearse an adverse action on the `fcra-records` order; the error cases (`order_not_complete`, `waiting_period_too_short`) are easier to meet in a test than in a real hire. Webhooks fire from the sandbox too: point `callbackUrl` at a tunnel to your machine or at the [webhook tester](/tools/webhook-tester). The [FCRA sandbox](/docs/fcra/sandbox) page and the [sandbox and errors guide](/docs/guides/fcra/sandbox-and-errors) list the other scenarios. Sandbox keys are limited to 60 calls a minute and 2,000 a day.

## What this background check API does not provide for HR screening

This API covers sex offender registry records from 58 US registries, criminal records (court cases, jail bookings, custody, warrants) and healthcare exclusions. It does not provide:

- Employment or education verification.
- Drug testing.
- Motor vehicle records (MVR).
- A Social Security number trace; no SSN is collected or returned.
- Credit reports.

Those come from other providers and sit beside this order on the candidate record. How a court case differs from a booking or a custody record is on [what shows up on a background check](/what-shows-up-on-a-background-check) and the [criminal record object](/docs/criminal/record-object) reference.

## Integrate the background check order into an ATS step by step

1. **Register the hiring employer as an end user.** Once per employer on your platform, `POST /v1/fcra/end-users` with its `permissiblePurposes` (`["employment"]`) and certification. Store the returned `id` (`eu_…`) on the employer record.
2. **Collect consent at the offer stage.** Record when and how the candidate authorised the check (`obtainedAt`, `method`, `documentRef`), or use hosted consent. The order is refused without it.
3. **Create the order with an Idempotency-Key.** `POST /v1/fcra/orders` with `endUserId`, `permissiblePurpose: "employment"`, the consent, the subject with aliases and addresses, `useLocation`, the searches and your `callbackUrl`; key it on your requisition id. Store the `202` order `id` and `consumerPortal` on the candidate.
4. **Wait for order.completed.** Receive `order.completed` on the endpoint you registered with `PUT /v1/fcra/webhook-endpoint`, or poll `GET /v1/fcra/orders/{order_id}` until `status` is `complete`.
5. **Write the result onto the candidate record.** For each entry in `searches[]`, store `status`, `result` (`clear`, `records_found` or `incomplete`) and `recordCount`; fetch records with `GET …/searches/{search_id}` and the report with `GET …/report`.
6. **Route records_found into the adverse-action flow.** Do not let a recruiter decline from the record view. Read `GET …/adverse-items`, start `POST …/adverse-actions` with the items and the use location, and let the waiting period run before the decision is final.

Python and Node.js SDKs ship with the first registry release; see [SDKs](/sdk). Until then the raw HTTP calls above are the integration, and the [Postman collection](/postman) has every FCRA request ready to send.

## HR and ATS background check API questions developers ask

### Can I use the standard search endpoints for a hiring decision?

No. Every standard response carries `legal.notice` "Not a consumer report" and may not be used for any FCRA purpose, which includes deciding whether to hire. A hiring check is an FCRA order through FCRA Partner Access, with consent, the `employment` purpose, adverse action and the consumer portal. Consult counsel on your obligations.

### Who is the end user, my platform or the employer?

The employer making the hiring decision. Your platform is the partner holding the API key; each employer is registered once and its `eu_…` id is sent on every order it places.

### How long does an order take?

Minutes, not seconds; the order carries an `eta` with `p50` and `p90`. Use the `order.completed` webhook rather than holding a request open; poll only as a fallback.

### What happens if my ATS submits the same order twice?

With the same `Idempotency-Key` and body inside 24 hours, the second call returns the original order with `Idempotent-Replayed: true`. A different body on the same key is a `409 idempotency_conflict`. Key on your requisition or application id.

### Does a withheld record count as a finding?

No. `withheld` counts candidates the furnishing rules kept off the report, for reasons such as `insufficient_identifiers` or `not_current_at_source`. A search with `result: "clear"` and withheld counts is clear.

### Can the candidate see the report?

Yes. Every order carries `consumerPortal.url` and `accessCode`; the candidate reads the same report there and can open a dispute. Show that to the candidate, not to the recruiter.

## What this page does not cover

- The partner application and the account controls: [FCRA access](/docs/fcra/access) and [controls](/docs/fcra/controls).
- The full adverse-action call sequence with bodies: the [FCRA adverse action API tutorial](/docs/learn/fcra-adverse-action-api).
- Disputes and record corrections: [disputes](/docs/fcra/disputes) and the [disputes guide](/docs/guides/fcra/disputes).
- Tenant screening, which is the same order with `permissiblePurpose: "tenant"`: the [tenant screening tutorial](/docs/learn/tenant-screening-flow).
- Retry and timeout policy for the calls above: [rate limits, retries and idempotency](/docs/learn/rate-limits-retries-idempotency).

## Related

- [FCRA adverse action API tutorial](https://offendersearch.app/docs/learn/fcra-adverse-action-api)
- [Webhook example](https://offendersearch.app/docs/learn/webhook-example)
- [Monitor a person with webhooks](https://offendersearch.app/docs/learn/monitor-a-person-with-webhooks)
- [Tenant screening flow](https://offendersearch.app/docs/learn/tenant-screening-flow)
- [Rate limits, retries and idempotency](https://offendersearch.app/docs/learn/rate-limits-retries-idempotency)
- [FCRA orders reference](https://offendersearch.app/docs/fcra/orders)
- [FCRA adverse action reference](https://offendersearch.app/docs/fcra/adverse-action)
- [FCRA webhooks and events](https://offendersearch.app/docs/fcra/webhooks)
- [FCRA sandbox](https://offendersearch.app/docs/fcra/sandbox)
- [Employment screening API](https://offendersearch.app/employment-screening-api)
- [FCRA employment screening](https://offendersearch.app/fcra-employment-screening)
- [Learn APIs by example](https://offendersearch.app/docs/learn)
