# Hospitals and visitor management

> A name at the kiosk, a badge in seconds, a supervisor path for the one-in-four look-alike, and a clearance so the regular visitor is never stopped twice.

- **HTML:** https://offendersearch.app/docs/solutions/hospitals-visitor-management
- **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 visitor screening works in hospitals today

Hospitals are open buildings with locked rooms inside them. The Medicare hospital Conditions of Participation guarantee patients the right to receive the visitors they designate — "including, but not limited to, a spouse, a domestic partner (including a same-sex domestic partner), another family member, or a friend" — require written visitation policies, allow "clinically necessary or reasonable" restrictions, and forbid restricting visitation on the basis of race, color, national origin, religion, sex, gender identity, sexual orientation or disability (42 CFR 482.13(h)). The Joint Commission's Environment of Care standard EC.02.01.01 requires a hospital to identify individuals entering its facilities and to control access to and from areas it designates security-sensitive, leaving the hospital to decide who needs identification and how. Its 2022 workplace violence prevention requirements add a program led by a designated individual with an annual risk analysis (LD.03.01.01 EP 9), an annual worksite analysis (EC.02.01.01 EP 17), incident reporting and investigation (EC.04.01.01 EP 6) and staff training (HR.01.05.03 EP 29). The International Association for Healthcare Security and Safety publishes the profession's guidelines, including one on violent patient and visitor management.

### The desk, the kiosk and the units

Operationally this becomes a visitor management system (VMS): a kiosk or a staffed desk at each public entrance that scans or types a government ID, captures the visitor's name and often date of birth, checks the name against the hospital's own lists (restricted visitors, protective orders, prior incidents) and, in many hospitals, against the public sex offender registries, then prints a photo badge that expires that day. Security staff run it; the chief security officer owns the policy; registration and nursing see the badge. The screen takes seconds because the line is long.

The registry check is concentrated where the patients are children or infants. The National Center for Missing & Exploited Children's *For Healthcare Professionals: Guidelines on Prevention of and Response to Infant Abductions* (10th edition, 2014) recommends limiting maternity visitors to those who can give the mother's full name, photographing maternity-unit visitors, positively identifying visitors to nursery, maternity, NICU and pediatric units with a photo ID, running background checks "including a search of sex offender registries" on everyone entrusted to care for or transport infants and pediatric patients, and searching the public registries for any adult patient placed on a unit with pediatric patients. Children's hospitals that publish their badging policies describe exactly that flow: ID compared against the public registries at badge issue, a 24-hour badge, and a registered visitor either refused entry or limited to the patient's room and public areas with security's approval. One published account of such a program put the confirmation rate at about a quarter: three of four registry name matches turned out, on review, to be a different person. The supervisor step exists because of that number.

### Volunteers and clergy

Volunteers are screened as staff. Joint Commission standard HR.01.01.01 EP 4 requires the hospital to obtain and document criminal background checks on applicants "as required by law and regulation or hospital policy", and the Commission's guidance says volunteers who work in the same capacity as staff providing care, treatment and services are expected to have them when law or policy requires. Some states go further: Hawaii requires prospective direct patient access employees *and adult volunteers* of health care facilities, hospitals included, to consent to checks covering criminal history, the abuse registries, the sex offender registry and the nurse aide registry, with an annual name inquiry thereafter (Haw. Rev. Stat. § 321-15.2). The OIG's exclusion guidance applies to volunteers as to employees. No rule specific to clergy was found; hospitals generally run visiting chaplains and faith-community volunteers through the volunteer program, and credential employed chaplains through HR.

> **Named, not applied.** The standards and statutes above are cited so you can read them; what they require of a given hospital, unit or visitor policy is for your security leadership, compliance office and counsel. A visitor policy that restricts a registrant must still satisfy 42 CFR 482.13(h) and your state's rules, and that judgment is yours. The API returns public records with provenance; it does not make a hospital compliant.

## Where the API fits

| Workflow step | Endpoint | Fields that matter |
| --- | --- | --- |
| Kiosk or desk: name (and DOB or age) → screen → badge | `POST /v1/search` across all 58 US registries | `query.firstName`, `lastName`, `dob` or `age`; `subjectId` = your visitor id, `clientId` = campus; `deadlineMs` + `onDeadline: "partial"`; read `status`, `records[].matchState`, `matchBasis`, `images[0].url`, `sources[0].recordUrl` |
| Supervisor resolves a look-alike once | `POST /v1/clearances` | `product: "sex_offender"`, `subjectId`, `clientId`, `personRef`, `recordId`, `searchId`, `verdict: "not_this_person"`, `reviewer`, `note` |
| Repeat visitor | The same search with the same `subjectId` | `cleared[]` and `counts.clearedSuppressed` show the look-alikes already resolved; `clearance.stale` on a record means the cleared person re-surfaced because something changed |
| Pediatric unit: an adult patient placed with children (NCMEC recommendation) | `POST /v1/search` | Same fields; `subjectId` = patient id; a `partial` answer is "could not complete" |
| Volunteers and visiting clergy | Where your counsel treats it as an employment-purpose check: the [FCRA Partner API](/docs/fcra); otherwise `POST /v1/search` and `POST /v1/criminal/search` with `subjectId` = volunteer id | `FcraOrderCreate` with `permissiblePurpose: "employment"`; or the self-service fields above |
| Document a contested restriction | `POST /v1/report`; `live.evidence: "pdf"` for the registry's own page | `searchId`; `evidence[].downloadUrl` |
| Volunteer roster stays watched | `POST /v1/monitors`, `type: "person"` | `person`, `label` = volunteer id, `channels` |

Pattern for VMS integration: the kiosk or VMS posts the visitor to your middleware (or your VMS vendor calls the API directly), the middleware calls `POST /v1/search` with the visitor's id as `subjectId` and the campus as `clientId`, derives the desk state below, and returns it to the VMS — which prints the badge, holds for a supervisor, or shows "could not complete". Keep the API key in the middleware, never in the kiosk.

## Reference integration

`screen_visitor` is the kiosk call: one registry search with a 15-second target and `onDeadline: "partial"`, so the desk never waits on a slow registry and never mistakes a partial answer for a clear one. `supervisor_clears` records that a look-alike is not this visitor; the demonstration runs the same visitor twice to show the second visit coming back clear with `previously_cleared: 3`. Run it against the sandbox with an `os_test_` key.

**Python**
```python
"""Hospital visitor management — kiosk screening with clearances for repeat visitors.
Python 3.11+, requests. Runs against the sandbox with its fixed test names.

  OFFENDERSEARCH_API_KEY   os_test_… (sandbox) or os_live_… (production)
  OFFENDERSEARCH_BASE      https://sandbox.offendersearch.app (default here)

The desk flow: a visitor gives a name and (where your policy collects it) a date of birth or age;
the kiosk sends one search with subjectId = your visitor id and clientId = the campus; the screen
shows clear / review / match / could-not-complete; a supervisor resolves review states and records
a clearance so the same look-alike never stops this visitor again.
"""
from __future__ import annotations

import os
import time
from dataclasses import dataclass, field

import requests

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


@dataclass
class DeskState:
    state: str                     # clear | review | match | could_not_complete | refused | failed
    message: str                   # what the desk sees
    search_id: str | None = None
    candidates: list[dict] = field(default_factory=list)
    previously_cleared: int = 0    # look-alikes a supervisor already resolved for this visitor


def _post(path: str, body: dict) -> requests.Response:
    for attempt in range(3):
        r = requests.post(f"{BASE}{path}", headers=HEADERS, json=body, timeout=30)
        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


def screen_visitor(first: str, last: str, *, visitor_id: str, dob: str | None = None, age: int | None = None) -> DeskState:
    """One Sex Offender Search across all 58 US registries. A kiosk cannot wait for a slow registry:
    deadlineMs is the target and onDeadline "partial" returns what finished — the desk then shows
    could-not-complete, never clear."""
    query: dict = {"firstName": first, "lastName": last}
    if dob:
        query["dob"] = dob
    elif age is not None:
        query["age"] = age
    r = _post("/v1/search", {"query": query, "subjectId": visitor_id, "clientId": CAMPUS_ID,
                             "deadlineMs": 15000, "onDeadline": "partial"})
    if r.status_code == 422:
        return DeskState("refused", f"Check the entry: {r.json()['detail']}")
    if r.status_code != 200:
        return DeskState("failed", f"Screening unavailable (HTTP {r.status_code}); follow the manual procedure.")
    d = r.json()
    cleared = len(d.get("cleared", []))
    if not d["records"]:
        if d["status"] == "complete":
            return DeskState("clear", "No registrant matched. Print the badge.", d["searchId"], previously_cleared=cleared)
        unfinished = [s["source"] for s in d["sourceStatus"] if s["incomplete"]]
        return DeskState("could_not_complete", f"{len(unfinished)} registr{'y' if len(unfinished) == 1 else 'ies'} did not answer in time; "
                         "retry or follow the manual procedure.", d["searchId"], previously_cleared=cleared)
    strong = [x for x in d["records"] if x["matchState"] == "dob_match"]
    cards = [{
        "personRef": x["personRef"], "recordId": x["recordId"], "name": x["name"]["full"], "aliases": x["aliases"],
        "dob": x["dob"], "age": x["age"], "matchState": x["matchState"], "matchBasis": x["matchBasis"],
        "registeredBy": x["registrationState"], "photo": x["images"][0]["url"] if x["images"] else None,
        "registryPage": x["sources"][0]["recordUrl"],
    } for x in d["records"]]
    if strong:
        return DeskState("match", "A registry record matched on name and date of birth. Call the supervisor.", d["searchId"], cards, cleared)
    return DeskState("review", f"{len(cards)} possible match(es) on name only. Supervisor compares photo and details.", d["searchId"], cards, cleared)


def supervisor_clears(visitor_id: str, card: dict, search_id: str, *, reviewer: str, note: str) -> str:
    """The supervisor compared the visitor with the registry record and they are different people.
    Record it once; every later search for this visitorId at this campus suppresses that person."""
    r = _post("/v1/clearances", {
        "product": "sex_offender", "subjectId": visitor_id, "clientId": CAMPUS_ID,
        "personRef": card["personRef"], "recordId": card["recordId"], "searchId": search_id,
        "verdict": "not_this_person", "reviewer": reviewer, "note": note,
    })
    if r.status_code == 409:
        raise RuntimeError("person_not_in_search: cite the search that showed this person")
    if r.status_code == 404:
        raise RuntimeError("search_not_found: the search is older than the 14-day history; search again and clear from the new result")
    r.raise_for_status()
    return r.json()["id"]                                 # clr_…


if __name__ == "__main__":
    # Sandbox test names. "Name Only" returns three name-only candidates in three states.
    first_visit = screen_visitor("Name", "Only", visitor_id="visitor-20931", age=48)
    print(first_visit.state, "-", first_visit.message)                                     # review: 3 possible matches
    for card in first_visit.candidates:                                                      # the supervisor resolves each one
        print("  cleared", supervisor_clears("visitor-20931", card, first_visit.search_id,
                                             reviewer="security-desk-1", note="Photo and age do not match the visitor."))
    second_visit = screen_visitor("Name", "Only", visitor_id="visitor-20931", age=48)
    print(second_visit.state, "-", second_visit.message, "| previously cleared:", second_visit.previously_cleared)   # clear | 3
    print(screen_visitor("Exact", "Match", visitor_id="visitor-20932", dob="1980-01-01").state)   # match
    print(screen_visitor("Clear", "Visitor", visitor_id="visitor-20933", age=35).state)           # clear
```

**JavaScript**
```javascript
// Hospital visitor management — kiosk screening with clearances for repeat visitors. Node 18+ (global fetch).
//   OFFENDERSEARCH_API_KEY  os_test_… (sandbox) or os_live_… (production)
//   OFFENDERSEARCH_BASE     https://sandbox.offendersearch.app (default here)
//   CAMPUS_ID               your id for the campus — clientId, so one campus's clearances never reach another's
const API_KEY = process.env.OFFENDERSEARCH_API_KEY;
const BASE = process.env.OFFENDERSEARCH_BASE ?? "https://sandbox.offendersearch.app";
const CAMPUS_ID = process.env.CAMPUS_ID ?? "campus-main";
const HEADERS = { "X-API-Key": API_KEY, "Content-Type": "application/json" };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function post(path, body) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const r = await fetch(`${BASE}${path}`, { method: "POST", headers: HEADERS, body: JSON.stringify(body) });
    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");
}

// state: clear | review | match | could_not_complete | refused | failed
const desk = (state, message, extra = {}) => ({ state, message, searchId: null, candidates: [], previouslyCleared: 0, ...extra });

/** One Sex Offender Search across all 58 US registries. deadlineMs is the target; onDeadline "partial" returns what
 *  finished and the desk shows could-not-complete, never clear. */
export async function screenVisitor({ firstName, lastName, visitorId, dob, age }) {
  const query = { firstName, lastName, ...(dob ? { dob } : age != null ? { age } : {}) };
  const r = await post("/v1/search", { query, subjectId: visitorId, clientId: CAMPUS_ID, deadlineMs: 15000, onDeadline: "partial" });
  if (r.status === 422) return desk("refused", `Check the entry: ${JSON.stringify((await r.json()).detail)}`);
  if (!r.ok) return desk("failed", `Screening unavailable (HTTP ${r.status}); follow the manual procedure.`);
  const d = await r.json();
  const previouslyCleared = (d.cleared ?? []).length;
  if (d.records.length === 0) {
    if (d.status === "complete") return desk("clear", "No registrant matched. Print the badge.", { searchId: d.searchId, previouslyCleared });
    const unfinished = d.sourceStatus.filter((s) => s.incomplete).map((s) => s.source);
    return desk("could_not_complete", `${unfinished.length} registr${unfinished.length === 1 ? "y" : "ies"} did not answer in time; retry or follow the manual procedure.`, { searchId: d.searchId, previouslyCleared });
  }
  const candidates = d.records.map((x) => ({
    personRef: x.personRef, recordId: x.recordId, name: x.name.full, aliases: x.aliases, dob: x.dob, age: x.age,
    matchState: x.matchState, matchBasis: x.matchBasis, registeredBy: x.registrationState,
    photo: x.images[0]?.url ?? null, registryPage: x.sources[0].recordUrl,
  }));
  const strong = d.records.some((x) => x.matchState === "dob_match");
  return strong
    ? desk("match", "A registry record matched on name and date of birth. Call the supervisor.", { searchId: d.searchId, candidates, previouslyCleared })
    : desk("review", `${candidates.length} possible match(es) on name only. Supervisor compares photo and details.`, { searchId: d.searchId, candidates, previouslyCleared });
}

/** The supervisor compared visitor and record: different people. Every later search for this visitorId at this campus suppresses that person. */
export async function supervisorClears(visitorId, card, searchId, { reviewer, note }) {
  const r = await post("/v1/clearances", {
    product: "sex_offender", subjectId: visitorId, clientId: CAMPUS_ID,
    personRef: card.personRef, recordId: card.recordId, searchId, verdict: "not_this_person", reviewer, note,
  });
  if (r.status === 409) throw new Error("person_not_in_search: cite the search that showed this person");
  if (r.status === 404) throw new Error("search_not_found: older than the 14-day history; search again and clear from the new result");
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return (await r.json()).id;                              // clr_…
}

// Sandbox test names. "Name Only" returns three name-only candidates in three states.
const first = await screenVisitor({ firstName: "Name", lastName: "Only", visitorId: "visitor-20931", age: 48 });
console.log(first.state, "-", first.message);                                              // review: 3 possible matches
for (const card of first.candidates) {
  console.log("  cleared", await supervisorClears("visitor-20931", card, first.searchId, { reviewer: "security-desk-1", note: "Photo and age do not match the visitor." }));
}
const second = await screenVisitor({ firstName: "Name", lastName: "Only", visitorId: "visitor-20931", age: 48 });
console.log(second.state, "-", second.message, "| previously cleared:", second.previouslyCleared);   // clear | 3
console.log((await screenVisitor({ firstName: "Exact", lastName: "Match", visitorId: "visitor-20932", dob: "1980-01-01" })).state);   // match
console.log((await screenVisitor({ firstName: "Clear", lastName: "Visitor", visitorId: "visitor-20933", age: 35 })).state);           // clear
```

*The second visit — records empty, three look-alikes in cleared[]*
```json
{
  "searchId": "srch_2e7d1a9c",
  "status": "complete",
  "counts": { "records": 0, "recordsReturned": 0, "sourcesQueried": 58, "sourcesComplete": 58, "sourcesIncomplete": 0, "clearedSuppressed": 3 },
  "warnings": [],
  "records": [],
  "cleared": [
    { "personRef": "prs_0a1b2c3d4e5f60718293", "clearanceId": "clr_7d41e2a9", "verdict": "not_this_person", "clearedAt": "2026-10-25T15:02:40+00:00", "recordId": "rec_6d066763131ebe8c77ea" },
    { "personRef": "prs_1b2c3d4e5f607182a3b4", "clearanceId": "clr_7d41e2aa", "verdict": "not_this_person", "clearedAt": "2026-10-25T15:02:41+00:00", "recordId": "rec_7e177874242fcf9d88fb" },
    { "personRef": "prs_2c3d4e5f607182a3b4c5", "clearanceId": "clr_7d41e2ab", "verdict": "not_this_person", "clearedAt": "2026-10-25T15:02:41+00:00", "recordId": "rec_8f288985353a0dae99ac" }
  ],
  "legal": { "notice": "Not a consumer report. This information may not be used for any purpose under the Fair Credit Reporting Act (15 U.S.C. § 1681 et seq.)." },
  "sandbox": true
}
```

### The desk states

| Desk state | Rule | What the desk and the supervisor see |
| --- | --- | --- |
| `clear` | `records` empty and `status: "complete"` | "Print the badge." The `searchId` is stored on the visit record. `previously_cleared` may be non-zero for a regular visitor. |
| `review` | Candidates exist, none with `matchState: "dob_match"` — typically a kiosk that collected only an age, or registries that publish no birth date (`no_dob_age_year`) | The kiosk holds; the supervisor sees one card per candidate — `name`, `aliases`, `dob`/`age`, `matchBasis`, `registeredBy`, the registry photo and the registry's own page — and compares it with the person and their ID. |
| `match` | A candidate with `matchState: "dob_match"` | The supervisor is called. Still a candidate until a person has compared photo and details; then the visitation policy applies — to the person, under 42 CFR 482.13(h). |
| `could_not_complete` | `records` empty but `status: "partial"`; `sourceStatus[]` rows with `incomplete: true` name the registries | Never shown as clear. Retry, or follow the manual procedure the policy defines for an outage. |
| `refused` / `failed` | `422` (read `detail` — a name too short to search, a malformed date), `429`, `5xx` | An operator message; the manual procedure. Nothing was billed. |

> **Note.** Collect a date of birth when you can. With one, a wrong-DOB look-alike is excluded before it reaches the desk (the sandbox's "Exact Match" with a different date returns nothing) and a true match is tiered `dob_match`. With only an age, common names produce `review` states; design for them rather than auto-denying. The [front-desk guide](/docs/front-desk-visitor-screening) walks the desk states with the sandbox scenarios.

## Ongoing monitoring

Visitors come and go; monitoring is for the people who stay. The volunteer roster — hundreds of people at a large hospital, many of them in pediatric and maternity areas — is the natural set: one person monitor per volunteer per dataset (`product: "sex-offender"` and, where your policy includes it, `"criminal"`), `label` = volunteer id, alerts to the volunteer office or security by webhook or email. Inpatients on a long stay in a unit with children are a second set, created at placement and cancelled at discharge. A location monitor around the campus (`type: "location"`, `radiusMiles`) is a third, for the security office's awareness.

*Create a person monitor (volunteer or long-stay patient)*
```bash
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": "resident-48213 · unit 2B",
    "channels": { "webhookUrl": "https://your-app.example.com/hooks/offendersearch?token=REPLACE_WITH_A_SECRET" }
  }'
```

- Evaluated once a day; the first evaluation reports every current match as `new`. Deliveries are signed with the monitor's `webhookSecret` — verify `X-Offendersearch-Signature` before acting.
- An alert names the person, the registry and the city, the `matchLabel`, and (for Sex Offender monitors) a `recordId` you can open with `GET /v1/records/{recordId}` before anyone acts.
- Cancel at the end of the volunteer term or at discharge; the monitor and its alerts remain readable.

[Person monitoring](/docs/monitoring/person-monitoring), [alerts](/docs/monitoring/alerts), [delivery](/docs/monitoring/delivery).

## When this is FCRA

Visitor screening is a facility-safety process under your visitation policy and 42 CFR 482.13(h); it is not a consumer report, and the self-service API's `legal.notice` applies. Screening a prospective employee is an employment purpose and belongs on the [FCRA Partner API](/docs/fcra) — an end user, a certified purpose, a consent attestation, controls, disputes and signed webhooks, available to approved partners (`403 fcra_not_enabled` otherwise). Volunteers and clergy sit between: the Joint Commission expects them to be screened like staff when law or policy requires, and whether that screen is a consumer-report use is a question for your counsel. Where counsel says it is, the volunteer check is an FCRA order with `permissiblePurpose: "employment"`; where it is not, the self-service search and criminal endpoints with `subjectId` = volunteer id are the fit. Never use the self-service API as the sole basis for a hiring, retention or volunteer-placement decision your counsel has placed under the FCRA.

## Evidence and audit

A restricted visitor may complain; a surveyor may ask how the visitation policy was applied; a family may ask who was badged onto the unit. The visit record should carry the `searchId`, the desk state, the `status` and `counts`, and — when a supervisor was involved — the clearance or the decision with the reviewer's name and note.

*Print the registry's own page for a contested restriction*
```bash
curl -X POST https://sandbox.offendersearch.app/v1/search \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "query": { "firstName": "Exact", "lastName": "Match", "dob": "1980-01-01" },
    "live": { "jurisdictions": ["OH"], "evidence": "pdf" }
  }'
# 202 { "searchId": "evs_…", "status": "pending", "mode": "async", "registries": ["OH"], "pollUrl": "/v1/searches/evs_…" }
# GET pollUrl until status is complete; evidence[] then carries one document per registry with a downloadUrl.
```

- **Verification report.** `POST /v1/report` with the `searchId` (and `viewerName` = the supervisor) renders a timestamped PDF of every matching record with a source citation on each, +$0.02. Render it at decision time. [Reports](/docs/reports).
- **Registry page.** `live.evidence: "pdf"` with `live.jurisdictions` prints the named registries' own pages; the `202` returns a `pollUrl`, the completed search carries `evidence[].downloadUrl`, readable for 30 days, 25¢ per document. [Live evidence](/docs/live-evidence).
- **Clearances.** Each `not_this_person` verdict cites the `searchId` that showed the person and keeps `reviewer` and `note`; `GET /v1/clearances?subjectId=…&clientId=…&includeRevoked=true` lists a visitor's history, revoked ones included. [Clearances](/docs/clearances).
- **Retention.** Searches stay in the account history for 14 days (`GET /v1/searches/{searchId}/results`); the visit record and badge log on your side hold what your policy requires beyond that. Keep `subjectId` as your visitor id, never an MRN or a patient identifier.

## Rollout plan

1. **Decide the policy before the kiosk.** With security leadership and counsel: which entrances and units screen, what is collected (ID scan, DOB, age), what each desk state means, who the supervisor is, and how a confirmed match is handled under 42 CFR 482.13(h) and state law. The screen implements the policy; it does not define it.
2. **Sandbox the desk states.** [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 watch "Name Only" go from `review` to `clear` after the clearances. Time the calls; set `deadlineMs` to what the line can bear.
3. **Integrate the VMS.** Put the API call in middleware your VMS or kiosk calls; map the VMS visitor id to `subjectId` and the campus to `clientId`; return the desk state and the candidate cards; keep the key out of the kiosk.
4. **Build the supervisor view.** Candidate cards with the registry photo and `sources[].recordUrl`; "this person" / "not this person", the second wired to `POST /v1/clearances`; a note field. Log who decided.
5. **Pilot one entrance.** Switch to an `os_live_` key at one pediatric or maternity entrance, run alongside the existing process, and measure review volume, confirmation rate and queue time. Expect most registry matches to resolve as different people; tune DOB capture, not the API.
6. **Extend to volunteers and other entrances.** Add volunteer screening on the path counsel chose (FCRA order or self-service), create person monitors for the roster, then roll the kiosk flow to the remaining entrances, one `clientId` per campus.

## Related

- [Front-desk visitor screening guide](https://offendersearch.app/docs/front-desk-visitor-screening)
- [POST /v1/search](https://offendersearch.app/docs/search)
- [Clearances](https://offendersearch.app/docs/clearances)
- [Live evidence](https://offendersearch.app/docs/live-evidence)
- [Skilled nursing facilities](https://offendersearch.app/docs/solutions/skilled-nursing)
