# Home health and home care agencies

> A caregiver works alone in someone's home. Here is how agencies screen before the first visit and keep screening afterwards, and which API each step belongs on.

- **HTML:** https://offendersearch.app/docs/solutions/home-care
- **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

## How screening works in home care today

Home health agencies (skilled, Medicare-certified) and home care agencies (non-medical personal care, state-licensed) share one fact that shapes everything: the worker is alone with the client, in the client's home, with no supervisor in the next room. Screening is therefore front-loaded. A recruiter or HR coordinator runs the checks between offer and first visit, a scheduler will not assign a caregiver whose file is incomplete, and the agency's compliance officer owns the periodic re-checks. Agencies with many branches run this from a central applicant-tracking or caregiver-management system that already holds the identifiers the checks need — legal name, date of birth, the states the caregiver has lived and worked in.

### What the rules require

The Medicare home health Conditions of Participation set personnel qualifications by discipline (42 CFR 484.115) and require staff to be licensed, certified or registered as state law requires (42 CFR 484.100(b)); they do not themselves require a criminal background check. The OIG said so directly in 2015: "there are no Federal laws or regulations that require HHAs to conduct background checks" (OEI-07-14-00130), while its companion report found that 41 states required home health agencies to check prospective employees as of January 2014, that every sampled agency ran a pre-hire check, and that about half also re-checked after hire (OEI-07-14-00131). Hospice is the exception with a federal rule: 42 CFR 418.114(d) requires a criminal background check on every hospice employee with direct patient contact or access to patient records, requires hospice contracts to impose the same on contracted staff, and — where the state sets no requirement — requires the check within three months of employment for every state the person lived or worked in during the prior three years.

State law carries the detail, and the detail is about timing and registries. California's Home Care Services Consumer Protection Act (Health & Safety Code § 1796.10 et seq.) requires an affiliated home care aide to be listed on the Home Care Aide Registry before providing services (§ 1796.14(b)), after electronic fingerprint submission to the Department of Justice (§ 1796.23) and a clearance or exemption issued before registration (§ 1796.24). Illinois' Health Care Worker Background Check Act (225 ILCS 46; 77 Ill. Adm. Code 955) covers home health care aides and personal care assistants, requires registry verification before employment, and bars hiring or retaining a person with a disqualifying conviction absent a waiver. Washington requires state and FBI fingerprint checks for long-term care workers, allowing unsupervised work while the FBI result is pending if the state check is clear (RCW 74.39A.056). New York's Criminal History Record Check program (10 NYCRR Part 402) covers Article 36 home care agencies, keeps a temporary employee under documented supervision until results arrive, and sends a subsequent-arrest notification the agency must risk-assess. Texas home and community support services agencies must check criminal history and the nurse aide and Employee Misconduct Registries before hiring, re-search the registries at least every twelve months, and check volunteers before a first face-to-face client contact (Tex. Health & Safety Code § 250.003; 26 TAC § 558.247).

Agencies billing Medicare or Medicaid also screen against the OIG's List of Excluded Individuals/Entities and their state Medicaid exclusion list. The OIG's 2013 Special Advisory Bulletin notes the LEIE is updated monthly, that monthly screening "best minimizes" liability, and that exposure attaches whether the person is an employee, a contractor or a volunteer (42 CFR 1001.1901). Sex offender registry checks are a common component of the pre-hire package agencies buy from screening vendors, alongside the criminal check and the registries above.

### Cadence, and the client's household

The cadence that emerges is: everything before the first visit; a registry re-search annually where the state says so; a criminal re-check on a fixed cycle (annual or biennial is common) or on a trigger such as a subsequent-arrest notice; exclusion lists monthly. The household is the other half of the safety picture. OSHA's *Guidelines for Preventing Workplace Violence for Healthcare and Social Service Workers* (OSHA 3148, 2016) treat home visits as field work in private residences where engineering controls may be impossible, and its field-work checklist asks whether workers are told of past violent acts by particular clients, briefed about the area they will work in, able to refuse service in a hazardous situation, and always known to be somewhere by someone. OSHA says nothing about screening a client's household members; agencies address the household through the intake assessment and those field-safety practices.

> **Named, not applied.** These rules are cited so you can read them. Which apply to a given agency, role and worker, what they require before a first visit, how a result may be used, and whether a given check is a consumer report are decisions for your compliance officer and counsel. The API supplies records with provenance; it does not make an agency compliant.

## Where the API fits

| Workflow step | Endpoint | Fields that matter |
| --- | --- | --- |
| Register the agency (or each franchisee / branch entity) as the end user that holds the employment purpose | `POST /v1/fcra/end-users` | `name`, `externalId`, `permissiblePurposes: ["employment"]`, `certification` (`text`, `version`, `certifiedAt`, `certifiedBy`) |
| Pre-hire check before the first visit | `POST /v1/fcra/orders` → `202`, then webhook `order.completed` or `GET /v1/fcra/orders/{order_id}` | `permissiblePurpose: "employment"`, `consent` (`obtainedAt`, `method`, `documentRef`), `subject` (`firstName`, `lastName`, `dob`, `addresses[].state`), `useLocation.state`, `searches[]` (`sex_offender`; `criminal` with `exclusions: "all"`), `clientReference`, `Idempotency-Key` |
| Read the result | `FcraOrder.searches[]` | `status`, `result` (`clear`, `records_found`, `incomplete`), `recordCount`, `withheld{}` (reasons a record was not furnished), `records[]` |
| Re-screen on the cadence or on a trigger | The same order call; `POST /v1/fcra/searches/{search_id}/reverify` to re-verify a furnished record | A new `clientReference`; the earlier order stays on file |
| Monthly exclusion-list run | The criminal search of an FCRA order with `exclusions: "all"` | `dispositionStatus: "sanction"` marks an exclusion-list record; `conviction_only` is the default mode |
| Know when something changes on the active roster | `POST /v1/monitors`, `type: "person"`, one per dataset | `person`, `minConfidence`, `label` = caregiver id, `channels.webhookUrl`; an alert triggers the regulated re-check |
| Awareness around a client's home for visit planning | `POST /v1/monitors`, `type: "location"`; on demand, `GET /v1/map/viewport?center=&radiusMiles=` | `location.lat`/`lng` or `address`, `radiusMiles`; alerts `in_radius` |
| Separation or discharge | `DELETE /v1/monitors/{monitorId}` | Evaluation stops now |

The split is deliberate. A caregiver check decides employment, so it runs as an FCRA order; the self-service search and criminal endpoints carry a `legal.notice` that they are not consumer reports and must not be used for that decision. Monitoring is a signal, not a decision: an alert tells the compliance officer to run the regulated re-check now instead of waiting for the calendar.

## Reference integration

Two halves. `register_agency_end_user` and `caregiver_order` are the regulated check — partner-only on the live host (`403 fcra_not_enabled` otherwise), but every `os_test_` key is an enabled sandbox partner, so the module runs as written against the sandbox, where an order completes immediately and the outcome is chosen by the subject's last name (`Clear`, `Records`, `Withheld`, `Unavailable`, `Excluded`, `Sanctioned`). `watch_caregiver` and `watch_client_home` are Monitoring. The webhook receiver for alerts is in the next section.

**Python**
```python
"""Home health and home care — caregiver screening and roster monitoring. Python 3.11+, requests.

Two different APIs for two different decisions:
  caregiver_order()     the pre-hire and re-screen check — an FCRA order (partner-only on the live host;
                        every os_test_ key is an enabled sandbox partner, so this runs as written)
  watch_caregiver()     Monitoring: a standing person watch on each active caregiver
  watch_client_home()   Monitoring: a location watch around a client's home for the care team's awareness

  OFFENDERSEARCH_API_KEY   os_test_… (sandbox) or os_live_… (production)
  OFFENDERSEARCH_BASE      https://sandbox.offendersearch.app (default here)
"""
from __future__ import annotations

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 _call(method: str, path: str, body: dict | None = None, **headers) -> requests.Response:
    for attempt in range(4):
        r = requests.request(method, f"{BASE}{path}", headers={**HEADERS, **headers}, json=body, timeout=60)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        if r.status_code >= 500:
            time.sleep(2 ** attempt)
            continue
        return r
    return r


# ── 1. The regulated check: an FCRA order for the agency (the end user) ────────────────────────

def register_agency_end_user(name: str, external_id: str) -> str:
    """Once per agency you screen for. The agency is the end user that holds the employment purpose
    and whose certification you record; the text and version are your counsel's."""
    r = _call("POST", "/v1/fcra/end-users", {
        "name": name,
        "externalId": external_id,
        "permissiblePurposes": ["employment"],
        "certification": {
            "text": "End user certifies compliance with FCRA § 1681b(b) for employment purposes.",
            "version": "2026-01",
            "certifiedAt": datetime.now(timezone.utc).isoformat(timespec="seconds"),
            "certifiedBy": "compliance@your-agency.example.com",
        },
    })
    if r.status_code == 403:
        raise RuntimeError(r.json()["error"]["code"])     # fcra_not_enabled: the live host needs a partner agreement
    r.raise_for_status()
    return r.json()["id"]                                  # eu_…


def caregiver_order(end_user_id: str, caregiver: dict, *, consent_doc: str, client_reference: str) -> dict:
    """One consumer, one purpose, one consent attestation, both search types. Use it before the first
    visit and again on your re-screen cadence (the same call; the previous order stays on file)."""
    r = _call("POST", "/v1/fcra/orders", {
        "endUserId": end_user_id,
        "permissiblePurpose": "employment",
        "consent": {"obtainedAt": datetime.now(timezone.utc).isoformat(timespec="seconds"),
                    "method": "esign", "documentRef": consent_doc},
        "subject": {"firstName": caregiver["first"], "lastName": caregiver["last"], "dob": caregiver["dob"],
                    "addresses": [{"state": s} for s in caregiver.get("states", [])]},
        "useLocation": {"state": caregiver["work_state"]},
        "searches": [{"type": "sex_offender"}, {"type": "criminal", "exclusions": "all"}],
        "clientReference": client_reference,
    }, **{"Idempotency-Key": client_reference})
    if r.status_code == 403:
        raise RuntimeError(f"FCRA: {r.json()['error']['code']}")
    if r.status_code != 202:
        raise RuntimeError(f"HTTP {r.status_code}: {r.text[:300]}")
    order = r.json()                                       # status: received
    # Production reports by signed webhook (order.completed); polling is the fallback. The sandbox is done at once.
    for _ in range(60):
        if order["status"] in ("complete", "partial", "error", "cancelled"):
            break
        time.sleep(5)
        order = _call("GET", f"/v1/fcra/orders/{order['id']}").json()
    return order


def summarize_order(order: dict) -> str:
    """What the recruiter sees. The report itself is GET /v1/fcra/orders/{id}/report (json or pdf)."""
    lines = [f"order {order['id']} · {order['status']}"]
    for s in order["searches"]:
        withheld = ", ".join(f"{k}={v}" for k, v in (s.get("withheld") or {}).items()) or "none"
        lines.append(f"  {s['type']}: {s['status']} · result={s['result']} · records={s['recordCount']} · withheld: {withheld}")
    return "\n".join(lines)


# ── 2. Monitoring: know when something changes, then run the regulated re-check ────────────────

def watch_caregiver(caregiver: dict, *, caregiver_id: str, webhook: str) -> list[str]:
    """One monitor per dataset — watching both is two monitors. An alert is a signal to run
    caregiver_order() again, not a decision."""
    ids = []
    for product in ("sex-offender", "criminal"):
        r = _call("POST", "/v1/monitors", {
            "product": product, "type": "person",
            "person": {"firstName": caregiver["first"], "lastName": caregiver["last"], "dob": caregiver["dob"]},
            "minConfidence": "year_match",
            "label": f"{caregiver_id} · {product}",
            "channels": {"webhookUrl": webhook},
        })
        r.raise_for_status()
        ids.append(r.json()["id"])                         # mon_…
    return ids


def watch_client_home(lat: float, lng: float, *, client_id: str, email: str) -> str:
    """A location watch around the client's home: registrants within the radius, and changes to them.
    Context for the care team's visit planning — not information about the client or the caregiver."""
    r = _call("POST", "/v1/monitors", {
        "product": "sex-offender", "type": "location",
        "location": {"lat": lat, "lng": lng, "radiusMiles": 1},
        "label": f"{client_id} · home",
        "channels": {"email": email},
    })
    r.raise_for_status()
    return r.json()["id"]


def stop_watching(monitor_id: str) -> None:
    """When the caregiver leaves or the client is discharged. Evaluation stops now."""
    _call("DELETE", f"/v1/monitors/{monitor_id}").raise_for_status()


if __name__ == "__main__":
    eu = register_agency_end_user("Sample Home Care Agency", "agency-001")
    # Sandbox outcomes are chosen by the subject's LAST NAME: Clear, Records, Withheld, Unavailable, Excluded, Sanctioned.
    for last in ("Clear", "Records", "Sanctioned"):
        order = caregiver_order(eu, {"first": "Pat", "last": last, "dob": "1980-01-01", "states": ["OH"], "work_state": "OH"},
                                consent_doc=f"consent/{last.lower()}-2026-10-25.pdf", client_reference=f"hire-{last.lower()}-001")
        print(summarize_order(order))
        # Clear → every search clear; Records → one furnished record per search; Sanctioned → the administrative
        # exclusion is furnished because the criminal search asked for exclusions: "all".
    mons = watch_caregiver({"first": "Exact", "last": "Match", "dob": "1980-01-01"}, caregiver_id="cg-5121",
                           webhook="https://your-app.example.com/hooks/offendersearch?token=REPLACE_WITH_A_SECRET")
    print("caregiver monitors:", mons)                     # the sandbox files one 'new' alert at once for the sample registrant
    print("client home monitor:", watch_client_home(39.9612, -82.9988, client_id="client-7731", email="scheduling@your-agency.example.com"))
```

**JavaScript**
```javascript
// Home health and home care — caregiver screening and roster monitoring. Node 18+ (global fetch).
// caregiverOrder(): the pre-hire / re-screen check as an FCRA order (partner-only on the live host; every
// os_test_ key is an enabled sandbox partner). watchCaregiver() / watchClientHome(): Monitoring.
//   OFFENDERSEARCH_API_KEY  os_test_… (sandbox) or os_live_… (production)
//   OFFENDERSEARCH_BASE     https://sandbox.offendersearch.app (default here)
const API_KEY = process.env.OFFENDERSEARCH_API_KEY;
const BASE = process.env.OFFENDERSEARCH_BASE ?? "https://sandbox.offendersearch.app";
const HEADERS = { "X-API-Key": API_KEY, "Content-Type": "application/json" };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const now = () => new Date().toISOString().replace(/\.\d{3}Z$/, "+00:00");

async function call(method, path, body, extraHeaders = {}) {
  for (let attempt = 0; attempt < 4; attempt++) {
    const r = await fetch(`${BASE}${path}`, { method, headers: { ...HEADERS, ...extraHeaders }, body: body ? JSON.stringify(body) : undefined });
    if (r.status === 429) { await sleep(1000 * Number(r.headers.get("Retry-After") ?? 5)); continue; }
    if (r.status >= 500) { await sleep(1000 * 2 ** attempt); continue; }
    return r;
  }
  throw new Error("OffenderSearch unavailable after retries");
}

// 1. The regulated check: an FCRA order. The agency is the end user that holds the employment purpose.
export async function registerAgencyEndUser(name, externalId) {
  const r = await call("POST", "/v1/fcra/end-users", {
    name, externalId, permissiblePurposes: ["employment"],
    certification: { text: "End user certifies compliance with FCRA § 1681b(b) for employment purposes.", version: "2026-01",
      certifiedAt: now(), certifiedBy: "compliance@your-agency.example.com" },
  });
  if (r.status === 403) throw new Error((await r.json()).error.code);   // fcra_not_enabled: the live host needs a partner agreement
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return (await r.json()).id;                                           // eu_…
}

/** Before the first visit and again on your re-screen cadence — the same call; earlier orders stay on file. */
export async function caregiverOrder(endUserId, caregiver, { consentDoc, clientReference }) {
  const r = await call("POST", "/v1/fcra/orders", {
    endUserId, permissiblePurpose: "employment",
    consent: { obtainedAt: now(), method: "esign", documentRef: consentDoc },
    subject: { firstName: caregiver.first, lastName: caregiver.last, dob: caregiver.dob, addresses: (caregiver.states ?? []).map((state) => ({ state })) },
    useLocation: { state: caregiver.workState },
    searches: [{ type: "sex_offender" }, { type: "criminal", exclusions: "all" }],
    clientReference,
  }, { "Idempotency-Key": clientReference });
  if (r.status === 403) throw new Error(`FCRA: ${(await r.json()).error.code}`);
  if (r.status !== 202) throw new Error(`HTTP ${r.status}: ${(await r.text()).slice(0, 300)}`);
  let order = await r.json();                                           // status: received
  // Production reports by signed webhook (order.completed); polling is the fallback. The sandbox is done at once.
  for (let i = 0; i < 60 && !["complete", "partial", "error", "cancelled"].includes(order.status); i++) {
    await sleep(5000);
    order = await (await call("GET", `/v1/fcra/orders/${order.id}`)).json();
  }
  return order;
}

/** What the recruiter sees. The report itself is GET /v1/fcra/orders/{id}/report (json or pdf). */
export function summarizeOrder(order) {
  const lines = [`order ${order.id} · ${order.status}`];
  for (const s of order.searches) {
    const withheld = Object.entries(s.withheld ?? {}).map(([k, v]) => `${k}=${v}`).join(", ") || "none";
    lines.push(`  ${s.type}: ${s.status} · result=${s.result} · records=${s.recordCount} · withheld: ${withheld}`);
  }
  return lines.join("\n");
}

// 2. Monitoring: know when something changes, then run the regulated re-check.
/** One monitor per dataset — watching both is two monitors. An alert is a signal to run caregiverOrder() again, not a decision. */
export async function watchCaregiver(caregiver, { caregiverId, webhook }) {
  const ids = [];
  for (const product of ["sex-offender", "criminal"]) {
    const r = await call("POST", "/v1/monitors", {
      product, type: "person",
      person: { firstName: caregiver.first, lastName: caregiver.last, dob: caregiver.dob },
      minConfidence: "year_match", label: `${caregiverId} · ${product}`, channels: { webhookUrl: webhook },
    });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    ids.push((await r.json()).id);                                      // mon_…
  }
  return ids;
}

/** A location watch around the client's home — context for visit planning, not information about the client or caregiver. */
export async function watchClientHome({ lat, lng }, { clientId, email }) {
  const r = await call("POST", "/v1/monitors", {
    product: "sex-offender", type: "location", location: { lat, lng, radiusMiles: 1 },
    label: `${clientId} · home`, channels: { email },
  });
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return (await r.json()).id;
}

/** When the caregiver leaves or the client is discharged. Evaluation stops now. */
export async function stopWatching(monitorId) {
  const r = await call("DELETE", `/v1/monitors/${monitorId}`);
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
}

const eu = await registerAgencyEndUser("Sample Home Care Agency", "agency-001");
// Sandbox outcomes are chosen by the subject's LAST NAME: Clear, Records, Withheld, Unavailable, Excluded, Sanctioned.
for (const last of ["Clear", "Records", "Sanctioned"]) {
  const order = await caregiverOrder(eu, { first: "Pat", last, dob: "1980-01-01", states: ["OH"], workState: "OH" },
    { consentDoc: `consent/${last.toLowerCase()}-2026-10-25.pdf`, clientReference: `hire-${last.toLowerCase()}-001` });
  console.log(summarizeOrder(order));
  // Clear → every search clear; Records → one furnished record per search; Sanctioned → the administrative
  // exclusion is furnished because the criminal search asked for exclusions: "all".
}
console.log("caregiver monitors:", await watchCaregiver({ first: "Exact", last: "Match", dob: "1980-01-01" },
  { caregiverId: "cg-5121", webhook: "https://your-app.example.com/hooks/offendersearch?token=REPLACE_WITH_A_SECRET" }));
console.log("client home monitor:", await watchClientHome({ lat: 39.9612, lng: -82.9988 }, { clientId: "client-7731", email: "scheduling@your-agency.example.com" }));
```

*GET /v1/fcra/orders/{order_id} — the Sanctioned subject, trimmed*
```json
{
  "id": "fo_ee0b34c5b52fd35b",
  "status": "complete",
  "createdAt": "2026-10-25T14:10:02+00:00",
  "updatedAt": "2026-10-25T14:10:02+00:00",
  "completedAt": "2026-10-25T14:10:02+00:00",
  "cancelledAt": null,
  "clientReference": "hire-sanctioned-001",
  "endUserId": "eu_8a2f1c3d",
  "permissiblePurpose": "employment",
  "useLocation": { "state": "OH" },
  "controlVersion": "fcra-controls-2026-10-19.1",
  "liveScope": "relevant",
  "sandbox": true,
  "searches": [
    { "id": "fs_452884eecde9a1f2", "type": "sex_offender", "status": "complete", "result": "clear",
      "recordCount": 0, "withheld": {}, "incompleteReasons": [], "records": [] },
    { "id": "fs_452884eecde9a1f3", "type": "criminal", "status": "complete", "result": "records_found",
      "recordCount": 1, "withheld": {}, "incompleteReasons": [],
      "records": [ { "recordId": "sandbox:excl:a:5a17c0de00000002", "type": "criminal", "dispositionStatus": "sanction",
                     "retrievedAt": "2026-10-25T14:10:02+00:00", "corrections": [], "sandbox": true } ] }
  ]
}
```

### The outcome logic

| What the order says | Rule | What the recruiter sees |
| --- | --- | --- |
| Clear | Every search `status: "complete"` with `result: "clear"` and `withheld` empty | The file is complete for this check; the `orderId`, `completedAt` and `controlVersion` are stored with it and the scheduler may assign. |
| Records found | A search with `result: "records_found"`; `records[]` carries each furnished record with `matchBasis`, and for criminal records `dispositionStatus` and `cases[]` | The report (`GET /v1/fcra/orders/{order_id}/report`) and the adverse-action steps your FCRA process already defines. The API furnishes; your process decides. |
| Clear, with withheld records | `result: "clear"` but `withheld` has counts (e.g. `insufficient_identifiers`, `not_current_at_source`) | Nothing is furnished because the controls would not allow it; the counts explain why the report is shorter than a self-service search would be. Not a defect. |
| Incomplete | A search ending `status: "unavailable"` with `result: "incomplete"` and `incompleteReasons[]` | The sources could not be reached; the order completes anyway. Re-order later; do not treat as clear. |
| Refused | `403 fcra_not_enabled` / `fcra_purpose_not_allowed`, `404 unknown_end_user`, `422 end_user_suspended` | Account, agreement or end-user setup — nothing ran. |

> **Note.** The exclusion-list tier is a choice on the criminal search: `conviction_only` (default) furnishes exclusions whose basis is a conviction; `all` adds administrative exclusions such as a licence action; `none` furnishes no exclusion-list record. The sandbox `Sanctioned` subject is clear under the default and furnished under `all`. [FCRA results](/docs/fcra/results).

## Ongoing monitoring

A roster of several hundred caregivers changes under you: someone is excluded in March, someone is added to a registry in July, and the annual re-check is in November. Monitoring puts a standing person watch on each active caregiver — one monitor per dataset — evaluated once a day. When an alert arrives, the agency runs the regulated re-check (`caregiver_order` again, or `POST /v1/fcra/searches/{search_id}/reverify` on the furnished record) and lets its FCRA process take it from there. The alert itself is not the decision, and Monitoring carries the same not-a-consumer-report notice as the rest of the self-service API; ask counsel how an alert may be used.

*Receive monitor alerts and queue the re-check*
```javascript
// Express: receive Monitoring alerts. Every delivery is signed with the monitor's webhookSecret
// (returned once on the 201) — verify over the RAW body before parsing. Answer 2xx fast; do the work after.
import express from "express";
const app = express();
import crypto from "node:crypto";
const SECRET = process.env.OFFENDERSEARCH_WEBHOOK_SECRET;   // channels.webhookSecret from the 201
app.post("/hooks/offendersearch", express.raw({ type: "application/json" }), (req, res) => {
  const parts = Object.fromEntries(String(req.get("X-Offendersearch-Signature") ?? "").split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", SECRET).update(`${parts.t}.`).update(req.body).digest("hex");
  const ok = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300 &&
    parts.v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);
  const { type, event, alertId, monitorId, monitorLabel, product, detail } = JSON.parse(req.body);   // type: "monitor.alert"
  if (type === "monitor.alert" && event !== "removed") {
    // detail: { name, jurisdiction, locality, matchLabel, matchConfidence, personRef, recordId? (sex-offender),
    //           recordType? / externalId? / exclusion? (criminal) }
    queueRescreen({ caregiverId: monitorLabel.split(" · ")[0], alertId, monitorId, product, detail });
  }
  res.sendStatus(204);
});
app.listen(8080);
```

- Set `minConfidence: "year_match"` when you have a date of birth and want fewer name-only alerts; leave it unset to get the default (`year_match` with a DOB, `name_match` without).
- The first evaluation reports every current match as `new`; expect alerts immediately for anyone who already has a record, and treat them as the re-check trigger they are.
- A location monitor around a client's home alerts when a registrant's published address enters the radius (`matchLabel: "in_radius"`); the alert carries the registrant's name, jurisdiction and city, never a street address. It informs visit planning; it says nothing about the client or the caregiver.
- Cancel at separation or discharge. The monitor and its alerts stay readable; it is not charged on any invoice prepared after the cancel.

[Person monitoring](/docs/monitoring/person-monitoring), [location monitoring](/docs/monitoring/location-monitoring), [alerts](/docs/monitoring/alerts), [webhooks and lifecycle](/docs/guides/monitoring/webhooks-and-lifecycle).

## When this is FCRA

This is the industry where the core job is a consumer report. Deciding whether to hire a caregiver, keep them, or assign them to a client is an employment purpose under the Fair Credit Reporting Act, and a background report procured from a third party for that purpose is a consumer report. The self-service Sex Offender Search, Criminal Records and Monitoring APIs must not be the basis for that decision; every response says so in `legal.notice`. The path is the [FCRA Partner API](/docs/fcra), and what changes is the shape of the work, not just the endpoint:

- An **end user** (the agency) registered with its permissible purposes and a certification you record; orders can state only a purpose the end user holds and your partner agreement allows (`403 fcra_purpose_not_allowed` otherwise). [Access](/docs/fcra/access).
- A **consent attestation** on every order (`obtainedAt`, `method`, `documentRef`) and the subject's **addresses**, which drive which jurisdictions are searched and which state rules apply.
- **Controls** applied at furnish time — reporting-period limits, state registry withholding, the exclusion-list mode — with the `withheld{}` counts telling you what was held back and why, and a `controlVersion` on the order. [Controls](/docs/fcra/controls).
- **Disputes** (`POST /v1/fcra/orders/{order_id}/disputes`), a **consumer portal** the subject can use, and **signed webhooks** (`X-Offendersearch-Signature: t=…,v1=…`) for `order.completed`, `search.completed`, `dispute.*` and `record.corrected` events. [Disputes](/docs/fcra/disputes), [webhooks](/docs/fcra/webhooks).
- **Partner-only.** The live host answers `403 fcra_not_enabled` without a signed partner agreement; the sandbox treats every `os_test_` key as an enabled partner so you can build first.

Whether a particular check — an exclusion-list screen of current staff, a registry re-search a state requires annually — is a consumer-report use is a question for your counsel. Where it is, it runs as an FCRA order.

## Evidence and audit

- **The order is the record.** Store `orderId`, `createdAt`, `completedAt`, `permissiblePurpose`, `controlVersion`, `clientReference` and each search's `status`, `result`, `recordCount` and `withheld{}` with the caregiver file. A state surveyor asking "what did you check before the first visit, and when" is answered from that row.
- **The report.** `GET /v1/fcra/orders/{order_id}/report?format=pdf` (or `json`) renders the regulated report with the required notices; file the PDF with the hire. [Orders](/docs/fcra/orders).
- **Receipts.** `GET /v1/fcra/receipts/{order_id}` returns an Ed25519-signed receipt over the order's `completedAt`, `controlVersion` and report hashes — proof of what was produced, without any record content. [Evidence](/docs/fcra/evidence).
- **Evidence captures.** `GET /v1/fcra/orders/{order_id}/evidence` lists the source captures behind furnished records; each is downloadable by `capture_id`.
- **Retention.** No retention period is published for FCRA order data; keep your own copies of the report, receipt and order row under your records policy and your partner agreement. Monitoring alerts stay readable on the monitor (`GET /v1/monitors/{monitorId}/alerts`, up to 200).

## Rollout plan

1. **Confirm the regulated path.** With counsel, map each check your agency runs (pre-hire, periodic, registry re-search, exclusions) to "FCRA order" or "not a consumer report". Apply for partner access for the first group; it is an agreement, not a toggle. [Access](/docs/fcra/access).
2. **Build against the sandbox.** [Create an account](/sign-up), mint an `os_test_` key in [Dashboard → API keys](/dashboard/keys), run the module above against `https://sandbox.offendersearch.app`, and exercise every last-name outcome (`Clear`, `Records`, `Withheld`, `Unavailable`, `Excluded`, `Sanctioned`, `Dispute`). [FCRA sandbox](/docs/fcra/sandbox).
3. **Wire the webhook endpoint.** Set the FCRA webhook endpoint (`PUT /v1/fcra/webhook-endpoint`), verify `X-Offendersearch-Signature`, and handle `order.completed` and `record.corrected`. Set the Monitoring receiver with a secret token in its URL.
4. **Integrate the caregiver system.** Trigger `caregiver_order` from the offer stage of your applicant tracking or caregiver-management system; block scheduling until the order is `complete` with the outcome your policy accepts; store the order row and the report with the file.
5. **Pilot one branch live.** Switch that branch to an `os_live_` key under the partner agreement, run new hires through the order flow alongside the current vendor for a cycle, and compare outcomes and turnaround.
6. **Roster monitoring and the re-screen cadence.** Create person monitors for the active roster, route alerts to the compliance queue as re-check triggers, and schedule the periodic orders your states require. Cancel monitors at separation.

## Related

- [FCRA Partner API](https://offendersearch.app/docs/fcra)
- [FCRA orders](https://offendersearch.app/docs/fcra/orders)
- [FCRA sandbox](https://offendersearch.app/docs/fcra/sandbox)
- [Person monitoring](https://offendersearch.app/docs/monitoring/person-monitoring)
- [Location monitoring](https://offendersearch.app/docs/monitoring/location-monitoring)
