Developer docs · v1.0.0

Staffing agencies and HR platforms

A hire is an FCRA decision from the first click. This article maps the disclosure-to-adverse-action workflow onto FCRA orders, end users and signed webhooks, and shows where continuous workforce monitoring fits without becoming a decision on its own.

How screening works in staffing and HR today

Pre-hire screening is nearly universal: in the Professional Background Screening Association's 2021 industry survey, 95% of employers with U.S. locations reported running one or more types of background check, and 19% reported checking beyond the initial hire — up from 12% the year before. The workflow is set by the Fair Credit Reporting Act and runs inside the applicant-tracking or HR system. A recruiter advances a candidate (after a conditional offer where a fair-chance law requires it); the system presents the FCRA disclosure as a standalone document and captures the candidate's written authorization; it orders the report from a consumer reporting agency; components run — identity, criminal, registry, employment and education verification, driving record, drug screen as ordered — and a report returns with records or a clear result; the employer adjudicates against its criteria; if it intends to decline, it sends a pre-adverse action notice with a copy of the report and the Summary of Rights, waits a reasonable period, and then sends the adverse action notice. Staffing agencies run this at volume for temporary and contract placements, often with a client's criteria layered on their own; HR platforms embed it as a step in onboarding.

The rules, precisely. Before procuring a report for employment purposes, the user must make a clear and conspicuous written disclosure in a document consisting solely of the disclosure, and obtain the consumer's written authorization (15 U.S.C. § 1681b(b)(2)(A)). Before taking adverse action based in whole or in part on the report, it must provide a copy of the report and the written description of consumer rights prescribed by the Consumer Financial Protection Bureau (§ 1681b(b)(3)(A)). The statute sets no waiting period; FTC staff said in a 1997 opinion letter that five business days "appears reasonable", and courts have held a user to the period its own pre-adverse letter promised. After the decision, the adverse-action notice must identify the agency with its contact details, state that the agency did not make the decision and cannot explain it, and describe the right to a free copy within 60 days and to dispute (§ 1681m(a)). The agency, for its part, must keep reasonable procedures for maximum possible accuracy (§ 1681e(b)) and, for public-record items reported for employment, either notify the consumer contemporaneously or maintain strict procedures to keep them complete and up to date (§ 1681k); non-conviction items older than seven years are excluded except for positions paying $75,000 or more (§ 1681c(a), (b)(3)). The EEOC's 2012 Enforcement Guidance on arrest and conviction records asks employers to use a targeted screen on the nature of the offence, the time elapsed and the nature of the job, followed by an individualized assessment, and warns against excluding on arrests alone. Fair-chance (ban-the-box) laws in roughly 37 states, the District of Columbia and more than 150 localities, and the federal Fair Chance to Compete for Jobs Act for federal agencies and contractors, push the criminal-history inquiry to after a conditional offer. Staffing agencies are users of the report for their own placements and have been sued for sending the pre-adverse notice after the worker had already been removed; a client that supplies the criteria shares the exposure.

Continuous monitoring is the growth edge: a standing watch on the active workforce so that a new conviction or registration between hire and the next re-screen is seen. It raises its own questions — whether the original authorization covers ongoing checks, which state laws limit it — and in practice an alert is a trigger to run a compliant check, not a finding in itself.

The API gives the report; the process stays yours. Every hiring decision informed by a report is an FCRA decision, so this entire article runs on the FCRA Partner API. The disclosure, authorization, adjudication, pre-adverse and adverse-action steps are the user's obligations and are not performed by the API. The standard APIs are not consumer reports and must not be the sole basis for an employment decision. Counsel decides applicability and the waiting period.

Where the API fits

Hiring stepEndpointFields that matter
Confirm partner access and purposesGET /v1/fcra/partnerenabled, allowedPurposes, webhookConfigured; never a 403 — enabled: false is the state to show
Register each employer the reports are forPOST /v1/fcra/end-usersname, externalId (your client id), permissiblePurposes: ["employment"], certification; PATCH …/{end_user_id} to suspend or change purposes
Disclosure and written authorizationyour ATS / HR systemProduce the standalone disclosure and capture the authorization; keep its reference for consent.documentRef
Place the order after authorizationPOST /v1/fcra/orders + Idempotency-KeyendUserId, permissiblePurpose: "employment", consent (obtainedAt, method, documentRef), subject (firstName, lastName, full dob, aliases[], addresses[]), useLocation.state (the job), annualSalary, searches[], clientReference, optional callbackUrl
Learn the resultsigned webhook, GET /v1/fcra/orders/{order_id}search.completed, order.completed; searches[].result, recordCount, withheld{}, incompleteReasons[]; POST /v1/fcra/searches/{search_id}/reverify for an unavailable source
AdjudicateGET /v1/fcra/orders/{order_id}/searches/{search_id}Furnished records with matchBasis.state, dispositionStatus, per-charge fcraDisposition, retrievedAt/verifiedAt, captureId
Pre-adverse and adverse actionGET /v1/fcra/orders/{order_id}/report?format=pdfThe copy of the report for the candidate, with consumerPortal.url and accessCode from the order; the notices themselves are yours
Dispute from the candidatePOST /v1/fcra/orders/{order_id}/disputesrecordIds[], reason, consumerStatement, receivedAt (the 30-day clock); dispute.updated and record.corrected events
Watch the active workforcePOST /v1/monitorsproduct: "criminal" or "sex-offender", type: "person", label with client and worker ids; an alert triggers a new order, never a decision

Reference integration

The module registers an employer as an end user, places an employment order under an idempotency key after the authorization is on file, waits for completion (production code reads order.completed from the webhook; polling is the fallback), and maps the order to the next step: clear (nothing furnished), review (records furnished — adjudication, then the pre-adverse and adverse-action sequence if declining) or incomplete (a source could not be reached — reverify first). It also includes the signature check for the webhook. On the sandbox every os_test_ key is an enabled partner and the subject's last name selects the scenario (Clear, Records, Withheld, Unavailable, Dispute).

"""Pre-hire screening as an FCRA order — OffenderSearch FCRA Partner API, Python 3.11+, requests.

The agency (or the HR platform acting for an employer) is the partner; each employer the report is for
is an end user. One order = one candidate, one end user, one permissible purpose, one consent attestation.

Sandbox: every os_test_ key is an enabled sandbox partner and orders finish right after the 202.
  OFFENDERSEARCH_API_KEY=os_test_...  OFFENDERSEARCH_BASE=https://sandbox.offendersearch.app  python hire_order.py
"""
from __future__ import annotations

import hashlib
import hmac
import os
import time
from datetime import datetime, timezone

import requests

API_KEY = os.environ["OFFENDERSEARCH_API_KEY"]
BASE = os.environ.get("OFFENDERSEARCH_BASE", "https://sandbox.offendersearch.app")
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}


def _now() -> str:
The order.completed delivery: verify v1 over "<t>." + raw body before reading data
POST https://your-agency.example/hooks/offendersearch
Content-Type: application/json
X-Offendersearch-Event: order.completed
X-Offendersearch-Event-Id: fev_5f1e3340306f1d02
X-Offendersearch-Signature: t=1792000252,v1=5d41402abc4b2a76b9719d911017c592…

{"createdAt":"2026-10-25T14:04:12Z","data":{"orderId":"fo_ee0b34c5b52fd35b","status":"complete"},"id":"fev_5f1e3340306f1d02","type":"order.completed"}
GET /v1/fcra/orders/{order_id}/searches/{search_id} — what the adjudicator reads (trimmed)
{
  "id": "fs_9c03d7e1a4b6f258",
  "type": "criminal",
  "status": "complete",
  "result": "records_found",
  "recordCount": 1,
  "withheld": { "insufficient_identifiers": 1 },
  "incompleteReasons": [],
  "coverage": [
    { "source": "IL-SANGAMON", "name": "Sangamon County Circuit Court", "jurisdiction": "IL", "mode": "live",
      "status": "answered", "relevant": true, "restricted": false, "candidates": 2, "furnished": 1, "withheld": 1,
      "captureIds": ["fc_41a2c7e09b3d5a18"] }
  ],
  "records": [
    { "recordId": "crim:IL-SANGAMON:2019CF000412", "type": "criminal",
      "matchBasis": { "state": "dob_match", "identifiers": ["name", "dob"] },
      "dispositionStatus": "conviction", "retrievedAt": "2026-10-25T14:02:38Z", "verifiedAt": "2026-10-25T14:02:38Z",
      "cases": [ { "caseNumber": "2019CF000412", "charges": [ { "description": "Retail theft", "severity": "misdemeanor",
                   "disposition": "Guilty", "dispositionDate": "2019-11-04", "fcraDisposition": "conviction" } ] } ] }
  ]
}

What the adjudicator sees: each furnished record's matchBasis.state and identifiers; for criminal records dispositionStatus across the charges and each charge's fcraDisposition (so a dismissed count is never read as a conviction); fcraRedactions naming what the controls removed and why; coverage[] saying which sources answered live, which from a snapshot, and how many candidates were withheld (insufficient_identifiers, not_current_at_source, obsolete_non_conviction). That last list is how an empty report explains itself. Results & records, Controls & reason codes.

Ongoing monitoring

For a placed worker or an active employee, a person monitor is a standing watch on one dataset: product: "criminal" for new court, jail, prison or warrant records, product: "sex-offender" for a new registration; two monitors to watch both. Put the client and worker ids in label, set minConfidence: "dob_match" to alert only on a confirmed date of birth, and deliver to a webhook with a secret token in the URL (monitoring deliveries are not signed). An alert is a fact about a record, not a consumer report: the compliant response is to place a new FCRA order — with authorization that covers it — and adjudicate that. Cancel the monitor when the placement ends.

Watch one placed worker for new criminal records
curl -X POST https://sandbox.offendersearch.app/v1/monitors \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "product": "criminal",
    "type": "person",
    "person": { "firstName": "Felony", "lastName": "Record", "dob": "1975-06-15" },
    "minConfidence": "dob_match",
    "label": "client-7731 / worker-5520 (active placement)",
    "channels": { "webhookUrl": "https://your-agency.example/hooks/offendersearch-monitors?token=7d2f41c0" }
  }'

Alerts arrive as monitor.alert with event (new, changed, removed), the personRef, and for criminal monitors the matched record's recordType; GET /v1/monitors/{monitorId}/alerts is the history. Person monitoring, Webhooks, duplicates and cancelling.

When this is FCRA

All of it. Hiring, placing, retaining and promoting are employment purposes, and a report that informs them is a consumer report. The staffing agency or HR platform reaches the FCRA Partner API as an approved partner under a written agreement, or through its screening partner; without access every FCRA route answers 403 fcra_not_enabled. The end-user model is the hinge: the agency itself is an end user for its own placements, and each client employer whose criteria drive a decision is registered as its own end user with its own certification, so the report names who procured it. The adverse-action hand-off is a sequence the API supports but does not perform: the order's consumerPortal gives the candidate their copy and a dispute path; GET …/report?format=pdf is the copy that accompanies the pre-adverse notice; the Summary of Rights, the wait, and the final notice are the user's to send and time. If the candidate disputes, POST …/disputes starts the 30-day reinvestigation, dispute.updated reports its outcome, and record.corrected reaches every partner that received the record. Nothing in this flow makes an employer compliant on its own; it makes each step provable.

Evidence and audit

  • Order id per requisition. Store the fo_ id with the candidate and the clientReference; GET /v1/fcra/orders?clientReference=… finds it, with status filters for queue views.
  • Consent provenance. consent.documentRef on the order should point at the stored authorization; the order carries the attestation and the controlVersion it ran under.
  • Report hashes and receipt. Keep the PDF with X-PDF-SHA256 and X-Report-SHA256; GET /v1/fcra/receipts/{order_id} is the public, Ed25519-signed receipt binding order, time, control version and hashes — verifiable without a key.
  • Evidence captures. GET /v1/fcra/orders/{order_id}/evidence lists every official-source capture (source, kind, sha256, retrievedAt), including from superseded searches; the bytes are at …/evidence/{capture_id}. No retention period is published; copy what your policy requires.
  • Event log. GET /v1/fcra/events?after=… pages every event with its delivery status and attempts; POST /v1/fcra/events/{event_id}/resend replays one.
  • Disputes. Each dispute's receivedAt, dueAt, extendedAt, status and outcome are on GET /v1/fcra/disputes/{dispute_id} — the record of the 30-day clock.

Rollout plan

  1. Access. Apply for FCRA Partner Access with the purposes you need; until approval, GET /v1/fcra/partner returns enabled: false on the live host. Build in the sandbox meanwhile, where every os_test_ key is an enabled partner.
  2. Consent flow. Produce the standalone disclosure and capture written authorization in your ATS or HR system before any order; store a reference you can place in consent.documentRef.
  3. End users. Register the agency and each client employer with certification text your compliance team approves; suspend end users whose agreements lapse.
  4. Orders and webhooks. PUT /v1/fcra/webhook-endpoint, store the one-time secret, verify signatures on the raw body, and drive your pipeline from search.completed and order.completed. Use Idempotency-Key on every create.
  5. Adjudication UI. Show dispositionStatus, per-charge fcraDisposition, withheld{} and coverage[]; generate the pre-adverse packet with the PDF and the consumer portal access code; enforce your waiting period before the adverse-action notice.
  6. Workforce monitors. Create person monitors on placement, route alerts to a new-order queue, and cancel on end of assignment.
  7. Go live. Place the first live order with a real consent record; confirm the receipt verifies and the webhook signature checks. Sandbox, Errors.