Developer docs · v1.0.0

Background-check and screening platforms

One integration serving many clients: per-tenant isolation, batch and asynchronous intake, reviewer verdicts that stick, usage you can reconcile, and a precise hand-off to the FCRA Partner API for regulated products.

How screening works in screening platforms today

A background-screening platform sells reports and workflow to many clients at once: employers, property managers, gig marketplaces, care providers, volunteer organisations, dating and sharing-economy apps. The platform receives an order from a client system — an applicant-tracking system, a property-management system, a marketplace back office, or the platform's own portal — collects the subject's identifiers (name, date of birth, addresses, sometimes an alias list), runs the components the client ordered, applies quality review to anything that returns a record, assembles a report, and pushes a status and a result link back to the client system. Two integration shapes dominate: the client system sends the complete order in one call, or it sends the minimum and the platform emails the subject a link to finish the form and give consent. Status flows back through polling or webhooks, and results land in the client's candidate or applicant record.

The regulatory line that shapes the whole industry is whether a given report is a consumer report under the Fair Credit Reporting Act. When a report is used for employment, housing, credit or insurance eligibility, the platform (or the consumer reporting agency behind it) owes the FCRA duties: a permissible purpose for each report and a certification from the end user (15 U.S.C. § 1681b and § 1681e(a)); reasonable procedures to assure maximum possible accuracy (§ 1681e(b)); for public-record information reported for employment purposes, either contemporaneous notice to the consumer or strict procedures to keep the information complete and up to date (§ 1681k); a summary of consumer rights with each report (§ 1681g(c)); and a reinvestigation path for disputes (§ 1681i). The user of the report owes the disclosure, authorization and adverse-action steps described in the FCRA access and end-user pages and the orders reference, and in the staffing and property-management articles in this section. Whether a particular product is a consumer report is a question of use, not of data: the same registry record can be furnished in a hiring report or shown to a hospital front desk, and only the first is regulated by the FCRA.

Many platform products are not consumer reports: resident and visitor screening for facilities, vendor and contractor on-site access, trust and safety checks inside a marketplace or dating product, and internal risk review. Those run under the platform's own policies and under state rules on how registry information may be used — notably California Penal Code § 290.46(j), Nevada NRS 179B.270 and New Jersey N.J.S.A. 2C:7-16(c), which list purposes (insurance, loans, credit, education, housing, and in California and Nevada employment) for which information from the state's public registry site may not be used, with exceptions for uses authorised by other law. What each rule requires is stated here; whether it applies to a given client and product is for the client's counsel.

Two products, one integration. The self-service Sex Offender and Criminal Records APIs are not consumer reports and must not be the sole basis for an employment, housing, credit or insurance decision. Products that make those decisions run through the FCRA Partner API, which is a different set of endpoints with an end-user model, consent attestation, signed webhooks and a dispute path. A platform typically runs both: self-service searches for its non-regulated products and FCRA orders for its regulated ones, under one account.

Where the API fits

Platform stepEndpointFields that matter
A client's user runs one check and waitsPOST /v1/searchquery.firstName, query.lastName, query.dob; subjectId (your id for the person) and clientId (your id for the client) on every call; read status, counts.sourcesIncomplete, records[].matchState, records[].matchDetail, cleared[]
Roster or file intake, up to 1,000 peoplePOST /v1/batchJSON queries[] or a text/csv body; subjectId per row, clientId once at the envelope; results in input order; rows carry no searchId and no sourceStatus — read counts.sourcesIncomplete and warnings[] per row
Unattended re-screens, scheduled jobsPOST /v1/searches → GET /v1/searches/{searchId}webhookUrl (unsigned — put a secret token in the URL); 202 body resultsUrl; poll until status leaves pending/running
Criminal records componentPOST /v1/criminal/searchquery.lastName required; subjectId/clientId work the same way; 202 with poll only when a live block fans out; refusals are {"error": {"code", "message"}}; meterEvents[] on every response
A reviewer decides a candidate is or is not the subjectPOST /v1/clearancesproduct (sex_offender or criminal), subjectId, clientId, personRef, searchId (within 14 days), verdict; GET /v1/clearances?subjectId=…&clientId=… lists them
Keep a client's roster currentPOST /v1/monitorsproduct + type; label carrying your tenant id; channels.webhookUrl or emails[]
Regulated products (employment, housing)POST /v1/fcra/end-users, POST /v1/fcra/ordersOne end user per client with permissiblePurposes and a certification; one order per consumer with permissiblePurpose, consent, subject, searches[]
Usage reconciliation per clientmeterEvents[], Dashboard → UsageCriminal responses always carry meterEvents[]; sex offender responses only when a live block ran. Regular sex offender searches reconcile from the dashboard, keyed by the clientId you log beside each searchId

Isolation lives in the identifiers, not in keys. Send the same clientId for everything one client does and that client's clearances never touch another client's results; both ids are stored only as per-account hashes. Issue separate API keys only where you need separate revocation. The platform data model and clearances reference have the full field lists.

Reference integration

A complete module for the sex offender component: an interactive check, roster intake through batch, an unattended re-screen through the asynchronous endpoint, and the reviewer verdict that makes a false positive disappear for that client from then on. The outcome rule is the one every tenant shares: clear is a complete answer with no records; incomplete is no records but at least one registry that did not finish (never shown as clear); match is a record whose full date of birth agreed (matchState: "dob_match") and whose legal name matched exactly on both fields (matchDetail.strategies, matchedNameType: "legal"); everything else is review. Run it against the sandbox with an os_test_ key — the names are the sandbox test names.

"""Multi-tenant screening — OffenderSearch Sex Offender Search API, Python 3.11+, requests.

Run against the sandbox:
  OFFENDERSEARCH_API_KEY=os_test_...  OFFENDERSEARCH_BASE=https://sandbox.offendersearch.app  python screening.py

Every client of your platform is a clientId; every person a client screens is a subjectId.
Both are stored only as per-account hashes, so one client's clearances never reach another's results.
"""
from __future__ import annotations

import os
import time
from dataclasses import dataclass, field

import requests

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


@dataclass
class Outcome:
    state: str                      # "clear" | "review" | "match" | "incomplete"

What a reviewer sees for a review or match: each candidate's name, aliases, dob or birthYear or age, registrationState and addressStates, offenses[], images[], matchDetail and source.recordUrl — enough to compare against the subject's own documents. The reviewer's verdict goes back as a clearance citing the searchId that showed the person; the next search for that subject and client lists the person in cleared[] with counts.clearedSuppressed incremented, or keeps and annotates them in clearanceMode: "annotate" for an interface that greys cleared people out. A cleared person who re-surfaces with a stronger match or a changed record comes back with clearance.stale: true and a staleReason, and stays until the reviewer decides again.

Batch rows cannot be cited or cleared. A POST /v1/batch row has no searchId, so it cannot be opened with GET /v1/searches/{searchId}/results, rendered with POST /v1/report, or cited by a clearance. Use batch for the sweep; run POST /v1/search for any person a reviewer will look at. The criminal component has its own batch endpoint with the same shape.

Ongoing monitoring

Clients who hold a roster — a care provider's staff, a marketplace's active providers, a league's coaches — want to know when something changes between screens. A person monitor is one standing watch on one dataset: product: "sex-offender" or product: "criminal", so watching both is two monitors. Put your tenant and subject ids in label so an alert can be routed without a lookup, and deliver to a per-tenant or per-platform webhookUrl. Monitoring deliveries are not signed today; authenticate them with a secret token in the URL you register and confirm anything consequential by reading GET /v1/monitors/{monitorId}/alerts.

Create a person monitor for one tenant's subject
curl -X POST https://sandbox.offendersearch.app/v1/monitors \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "product": "sex-offender",
    "type": "person",
    "person": { "firstName": "Exact", "lastName": "Match", "dob": "1980-01-01" },
    "minConfidence": "year_match",
    "label": "tenant-acme-care / applicant-1001",
    "channels": { "webhookUrl": "https://your-app.example.com/hooks/offendersearch?token=9f2c1a7b" }
  }'
The monitor.alert delivery (sandbox: posted once, right after the 201)
{
  "type": "monitor.alert",
  "event": "new",
  "alertId": "hit_7fa176c8889c7569",
  "monitorId": "mon_b8f40d7afb9dc0d8",
  "monitorLabel": "tenant-acme-care / applicant-1001",
  "product": "sex-offender",
  "detail": {
    "name": "EXACT SAMPLE MATCH",
    "jurisdiction": "OH",
    "locality": "Sampleville",
    "matchLabel": "dob_match",
    "matchConfidence": 1.0,
    "personRef": "prs_3c9e1f0a7b2d4e6f8a1b",
    "recordId": "rec_6d066763131ebe8c77ea"
  },
  "occurredAt": "2026-10-25T12:00:41+00:00",
  "sentAt": "2026-10-25T12:01:03+00:00"
}

Alerts carry event (new, changed, removed), the personRef the alert is keyed on, and for sex offender monitors the recordId to open with GET /v1/records/{recordId}. Treat an alert as a trigger for a fresh search or an FCRA order, not as a result. Cancel with DELETE /v1/monitors/{monitorId} when the client offboards a person: evaluation stops at once and the monitor is not charged on any invoice prepared after the cancel. Alerts, Delivery, Billing.

When this is FCRA

If a product's result will help a client decide whether to hire someone or rent to them, the product is a consumer report and runs on the FCRA Partner API. Access is granted to approved partners under a written agreement; without it every FCRA route answers 403 fcra_not_enabled, and GET /v1/fcra/partner reports enabled: false without a 403 so you can show the state in your admin UI. What changes in the integration:

  • Your client becomes an end user. POST /v1/fcra/end-users with the client's name, its permissiblePurposes (employment, tenant_screening, consumer_written_instructions, legitimate_business_need) and your certification of it. A purpose outside your partner agreement is 403 fcra_purpose_not_allowed.
  • Each search becomes an order. POST /v1/fcra/orders names the endUserId, one permissiblePurpose, a consent attestation (obtainedAt, method, documentRef), the subject with a full date of birth, useLocation (the state the job or housing is in) and one or two searches[]. It answers 202 with status received; searches run in the background under the accuracy controls, and records that fail them are withheld with a reason rather than furnished.
  • Results come by signed webhook. PUT /v1/fcra/webhook-endpoint returns a secret once; every search.completed, order.completed, dispute.created and record.corrected delivery carries X-Offendersearch-Signature: t=…,v1=…, an HMAC-SHA256 over "<t>." + body. Verify before acting; GET /v1/fcra/events is the replayable log.
  • The report is the deliverable. GET /v1/fcra/orders/{order_id}/report as JSON or the stored PDF, with integrity.reportSha256, and a public Ed25519 receipt at GET /v1/fcra/receipts/{order_id} that anyone can verify without a key. Every order also carries consumerPortal.url and accessCode for the consumer's copy and dispute path.
  • Disputes have a clock. POST /v1/fcra/orders/{order_id}/disputes opens a reinvestigation due 30 days from receivedAt, with one 15-day extension through /information.

Integrating the FCRA Partner API does not make a platform or its clients compliant: the disclosure, authorization, adjudication, pre-adverse and adverse-action steps remain the user's, and the platform's own § 1681e procedures remain its own. The API gives those steps a report whose provenance can be shown. Counsel decides which products are consumer reports.

Evidence and audit

  • Persist every `searchId` with the clientId, subjectId, status, counts, sourceStatus[] and your outcome. That row is the proof of what was searched and when; it is also what a clearance must cite.
  • Re-open stored results with GET /v1/searches/{searchId}/results for the account's retention window (retentionDays, 14 days today); after that the endpoint answers 410 and the search must be run again.
  • Render a verification report with POST /v1/report for a search up to 7 days old: one consolidated PDF of every matching record and every field on file, with a citation per record and the use-restriction notice printed on it. The X-Report-Id header is the document id to store.
  • Capture the registry's own page when a client's policy requires primary evidence: live.evidence: "pdf" with live.jurisdictions answers 202 with pollUrl; the finished search carries evidence[] with a downloadUrl per registry, readable for 30 days. Live evidence.
  • Clearances are an audit trail. GET /v1/clearances?subjectId=…&clientId=…&includeRevoked=true returns every verdict with reviewer, note, createdAt, revokedAt and supersedes.
  • FCRA orders write their own evidence: GET /v1/fcra/orders/{order_id}/evidence lists every official-source capture with a SHA-256; the receipt binds the report hash to the order. No retention period is published for FCRA evidence; keep your own copies to your policy.

Rollout plan

  1. Sandbox. Create an account, mint a test key under Dashboard → API keys, and run the module above against https://sandbox.offendersearch.app. Exercise clear, review, match and incomplete, and a clearance round-trip, on the sandbox names.
  2. Tenant model. Decide the clientId grain (one per billing client) and the subjectId grain (one per person per client). Log both beside every searchId for reconciliation.
  3. Reviewer UI. Show candidates with photo, offenses, registry and matchDetail; record verdicts as clearances; surface clearance.stale for re-review.
  4. Volume paths. Wire POST /v1/batch for intake and POST /v1/searches with a tokenised webhookUrl for scheduled re-screens; handle 429 with Retry-After and treat partial as incomplete. Operations.
  5. Monitoring. Create person monitors per tenant subject with label routing; cancel on offboard.
  6. FCRA products. Apply for FCRA Partner Access. Build end-user registration, orders, signed-webhook verification and the report hand-off against the sandbox, where every os_test_ key is an enabled partner for every purpose.
  7. Go live. Switch the base URL and key. Confirm the first production searchId resolves on GET /v1/searches/{searchId}/results, the first meterEvents[] reconcile with Dashboard → Usage, and the first monitor alert reaches your webhook. Go-live checklist.