Sex Offender Search API · v1.0.0

Front-desk visitor screening integration

From the ID scan to the staff screen, the reviewer's verdict, and the roster you keep watching. A complete walkthrough against the sandbox.

What you will build

A visitor arrives at a school, a care facility or an office; a kiosk scans the ID; before the badge prints, the desk knows whether the name and date of birth match a registrant. You will build the four moving parts: the search at sign-in (keyed to your visitor and site ids), the desk state it maps to (Clear, Review, Match), the reviewer's verdict recorded as a clearance so a false positive is quiet next time, and a monitor on the people who visit regularly. The reference tables for this recipe — what to send from the ID, the staff screen, retention — live on Front-desk visitor screening; this guide is the build order.

Before you start

  • A test key. Sandbox visitors: Clear Visitor (Clear), Exact Match with dob 1980-01-01 (Match), Name Only (Review: three same-name candidates without birth dates).
  • Two ids from your own system on every search: subjectId (your stable id for the visitor — a badge number, a CRM id) and clientId (the site, school or district when one key serves several). They are stored only as per-account hashes and they are what clearances key on.
  • The desk-state rule: Match = a record with matchState: "dob_match" and matchConfidence ≥ 0.90 (an exact legal name with a confirmed full date scores 0.90 or more); Clear = status: "complete" and counts.records: 0; everything else is Review. The thresholds come from the scorer's own arithmetic, not from tuning.
The one thing people get wrong. A kiosk cannot decide. It can only sort into Clear, Review and Match and show the right screen to staff, never to the visitor. Review and Match are prompts for a person to compare the photo, age and address against the person standing there and to open sources[].recordUrl. Nothing in this flow auto-denies.

Step by step

  1. Search at sign-in. Send firstName, lastName and dob from the ID with subjectId and clientId. One POST /v1/search, typically well under a second.
  2. Map to a desk state. Compute Clear / Review / Match from status, counts.records and each record's matchState + matchConfidence. Store searchId, the state and the time.
  3. Record the reviewer's verdict. When a reviewer decides a candidate is not the visitor, POST /v1/clearances with the record's personRef, the searchId that showed it, and verdict: "not_this_person". The next search for that subjectId suppresses the person and lists them in cleared[].
  4. Watch the regulars. For visitors on a standing roster (contractors, volunteers, family members), create a person monitor so a new registration reaches you between visits.

Steps 1–3 — the desk module

"""desk.py — kiosk sign-in → Clear | Review | Match, and the reviewer's clearance."""
import os
from datetime import datetime, timezone
import requests

BASE = os.environ.get("OFFENDERSEARCH_BASE", "https://sandbox.offendersearch.app")
H = {"X-API-Key": os.environ["OFFENDERSEARCH_KEY"]}
MATCH_MIN_CONFIDENCE = 0.90

def _post(path, body):
    r = requests.post(BASE + path, headers=H, json=body, timeout=60)
    r.raise_for_status()
    return r.json()

def desk_state(result):
    if result["status"] != "complete":
        return "review"                      # incomplete: a person must see it; never Clear
    if result["counts"]["records"] == 0:
        return "clear"
    strong = any(r["matchState"] == "dob_match" and (r["matchConfidence"] or 0) >= MATCH_MIN_CONFIDENCE
                 for r in result["records"])
    return "match" if strong else "review"

def screen_visitor(visitor_id, site_id, first_name, last_name, dob):
Step 3 — POST /v1/clearances → 201
{
  "id": "clr_9c54032c347c4f1e",
  "product": "sex_offender",
  "verdict": "not_this_person",
  "personRef": "prs_fec5ef71d9deef5bc8c4",
  "recordId": "rec_fec5ef71d9deef5bc8c4",
  "searchId": "srch_e891a43dfb5b8648131fde66",
  "subjectId": "visitor-1044",
  "clientId": "facility-07",
  "reviewer": "front-desk",
  "note": "Photo and age do not match the visitor.",
  "createdAt": "2026-10-25T14:18:55.563989+00:00",
  "revokedAt": null,
  "supersedes": null,
  "sandbox": true
}
The next sign-in for the same visitor (trimmed)
{
  "status": "complete",
  "counts": { "records": 2, "recordsReturned": 3, "sourcesQueried": 58, "sourcesComplete": 58, "sourcesIncomplete": 0, "sourcesSkippedByScope": 0, "clearedSuppressed": 1 },
  "records": [
    { "recordId": "rec_ca91307a7124dca36777", "personRef": "prs_ca91307a7124dca36777", "registrationState": "GA", "matchState": "no_dob_age_year" },
    { "recordId": "rec_84881c51922112e393d2", "personRef": "prs_84881c51922112e393d2", "registrationState": "MI", "matchState": "no_dob_age_year" }
  ],
  "cleared": [
    { "personRef": "prs_fec5ef71d9deef5bc8c4", "clearanceId": "clr_9c54032c347c4f1e", "verdict": "not_this_person",
      "clearedAt": "2026-10-25T14:18:55.563989+00:00", "recordId": "rec_fec5ef71d9deef5bc8c4" }
  ]
}

The cleared person has left records[], counts.records is 2, and cleared[] lets the staff screen say "1 previously cleared person hidden". Send clearanceMode: "annotate" instead to keep the record with a clearance object for a greyed-out row. A clearance matches by personRef only — never by name — because the person being cleared shares the visitor's name. If that registrant's record changes materially, the clearance goes stale and the person comes back for review.

Step 4 — monitor the roster

POST /v1/monitors
{
  "product": "sex-offender",
  "type": "person",
  "label": "facility-07 · volunteer v-2201",
  "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" },
  "channels": { "webhookUrl": "https://example.com/hooks/monitors?token=YOUR_SECRET" }
}

One monitor per person per dataset (product is sex-offender with a hyphen here; a clearance's product is sex_offender with an underscore). Monitors are evaluated daily, alerts arrive by email or webhook, and a cancelled monitor stops at once. Billing is flat per active monitor (Monitoring billing). The build details are on Person monitoring.

Reading the result

Desk stateRuleWhat staff seeWhat is stored
Clearstatus: "complete" and counts.records: 0Nothing — the badge prints.searchId, state, checkedAt.
ReviewAny records, none dob_match at ≥ 0.90; or status: "partial"Candidate cards: name, aliases, photo, age, registration state, matchState, a link to the registry page. "Not this person" and "This is the person" buttons.searchId, each candidate's personRef and recordId, the verdict, the reviewer.
MatchA record with matchState: "dob_match" and matchConfidence ≥ 0.90The same card, flagged, with your escalation procedure.The above, plus a verification report requested within 7 days.
cleared[]Present when subjectId was sent"N previously cleared people hidden", each linking to its clearance.clearanceIds for the audit.

Edge cases and pitfalls

  • No `dob` from the ID means no Match state is reachable. A record found on name alone has matchState: null even at 0.90. Treat name-only sign-ins as Review and ask for the date of birth.
  • `partial` is Review, not Clear. The sandbox never produces it; test the branch with a fixture. See Working with incomplete results.
  • Multi-site platforms must send `clientId`. Without it, one site's clearances apply at every site the key serves. A verdict recorded with clientId is listed only with the same clientId.
  • A clearance must cite the search that showed the person within the 14-day window. Citing another search answers 409 person_not_in_search; citing an erased one answers 404 search_not_found. Clear from the result on screen, at the time.
  • `this_person` is also a verdict. Recording it annotates the record on later searches rather than hiding it, so the next desk sees that the match was confirmed before.
  • Keep the kiosk honest about time. checkedAt is yours; sources[].lastCheckedAt is when the registry was last read. Both belong in the log.
  • This is not a consumer report. Visitor screening is not an employment, housing, credit or insurance decision, and the API must not be used as the sole basis for one; that path is the FCRA Partner API.

Production checklist

  • Staff-only display; the visitor never sees a result.
  • subjectId and clientId on every search; the same ids used for clearances and monitors.
  • Review and Match screens link sources[].recordUrl and record the reviewer's verdict as a clearance.
  • Match triggers a verification report within 7 days; searchId, recordId, personRef, clearanceId and X-Report-Id are stored together.
  • Standing visitors monitored; alerts routed to the same review queue.
  • The remaining checks in the Go-live checklist.

Product overview