Developer docs · v1.0.0

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.

Updated November 3, 2026

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 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, 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 page and how it works describe the partner relationship; the FCRA API page is the product summary.

You want to…EndpointWhy
Decide whether to hire, promote or retainPOST /v1/fcra/ordersA consumer report: consent, permissible purpose, adverse action, consumer portal.
Run a lookup that drives no employment decisionPOST /v1/search, POST /v1/criminal/searchStandard API; legal.notice applies.
Be told when a watched person appears on a registryPOST /v1/monitorsMonitoring 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 describes the employer's certification.

Register the employer once
# 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.

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 -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"
  }'

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 issues an invitation the candidate completes. The full body, liveScope and the search types are on the orders reference and the creating orders guide.

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
# 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 page; GET /v1/fcra/events lists every event with its delivery state, and the FCRA webhooks reference 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)
{
  "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 and 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 walks through the calls with bodies, the adverse action reference lists the error codes (order_not_complete, individualized_assessment_required, waiting_period_too_short), and the 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 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). The monitoring tutorial builds the receiver.
  • Developer tooling and demos against the 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 page is the product summary for this audience; 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.

SubjectScenarioWhat the order returns
Pat Clearfcra-clearOrder complete; every search complete with result clear and recordCount 0.
Pat Records, 1980-01-01fcra-recordsOrder complete; each search records_found with one sandbox record (the criminal one carries cases[] with a misdemeanor conviction).
Pat Withheld, 1980-01-01fcra-withheldCriminal 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. The FCRA sandbox page and the sandbox and errors guide 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 and the 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. Until then the raw HTTP calls above are the integration, and the Postman collection 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