Developer docs · v1.0.0

Integration recipes

Everything required for a working implementation, in one place per task. Fictional names; run against the sandbox first.

Build a sex offender screening integration (Python)

# Sex offender screening — Python 3.11+, requests. Env: OFFENDERSEARCH_API_KEY, OFFENDERSEARCH_BASE
import os, time, requests

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

def search(first: str, last: str, dob: str | None = None, state: str | None = None) -> dict:
    body = {"query": {"firstName": first, "lastName": last, **({"dob": dob} if dob else {})}}
    if state:
        body["jurisdictions"] = [state]
    for attempt in range(4):
        r = requests.post(f"{BASE}/v1/search", headers=H, 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
        r.raise_for_status()
        return r.json()
    raise RuntimeError("unavailable")

def classify(data: dict) -> str:
    if not data["records"]:
        return "clear" if data["status"] == "complete" else "incomplete"
    strong = [x for x in data["records"] if x.get("matchState") == "dob_match"]  # DOB corroborated; gate on matchState, not on matchConfidence alone
    return "match" if strong else "review"

if __name__ == "__main__":
    data = search("Jordan", "Rivera", "1988-04-12")
    print(classify(data), data["searchId"], [s["source"] for s in data["sourceStatus"] if s["incomplete"]])

Fields read: status, searchId, sourceStatus[].source / .incomplete, records[].matchState / .matchConfidence. Reference: POST /v1/search.

Integrate criminal search into a Node.js application

// Criminal search in a Node.js service — Node 18+. Env: OFFENDERSEARCH_API_KEY, OFFENDERSEARCH_BASE
const API_KEY = process.env.OFFENDERSEARCH_API_KEY;
const BASE = process.env.OFFENDERSEARCH_BASE ?? "https://api.offendersearch.app";
const H = { "X-API-Key": API_KEY, "Content-Type": "application/json" };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function criminalSearch({ firstName, lastName, dob, state, page = 1 }) {
  const body = { query: { lastName, firstName, ...(dob ? { dob } : {}), ...(state ? { state } : {}) }, page, perPage: 50 };
  let r = await fetch(`${BASE}/v1/criminal/search`, { method: "POST", headers: H, body: JSON.stringify(body) });
  if (r.status === 429) { await sleep(1000 * Number(r.headers.get("Retry-After") ?? 5)); return criminalSearch({ firstName, lastName, dob, state, page }); }
  if (r.status === 402) throw new Error("trial used up or no active subscription");   // body: { detail }
  if (r.status === 422) throw new Error(`refused: ${JSON.stringify((await r.json()).error)}`);  // { error: { code, message } }
  if (r.status === 202) {                      // more than two live sources: the job runs in the background
    const { poll } = await r.json();           // "/v1/criminal/searches/crs_…"
    for (let i = 0; i < 150; i++) {            // every 2 s, up to 5 minutes
      await sleep(2000);
      const p = await fetch(`${BASE}${poll}`, { headers: H });
      if (p.status === 202) continue;          // JobRecord, status "pending", result null
      if (p.status === 422) throw new Error("refused when run: the stored query was too broad");
      if (!p.ok) throw new Error(`poll HTTP ${p.status}`);
      return (await p.json()).result;          // 200: JobRecord — the envelope is under result
    }
    throw new Error("live check did not finish");
  }
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
  return r.json();
}

export function summarise(data) {
  // Each record: recordType, currentStatus, cases[].charges[] with description/statute/severity/disposition.
  return data.records.map((rec) => ({
    type: rec.recordType,
    status: rec.currentStatus,
    confidence: rec.matchConfidence,
    charges: (rec.cases ?? []).flatMap((c) => (c.charges ?? []).map((ch) => ({
      description: ch.description, statute: ch.statute, severity: ch.severity, disposition: ch.disposition,
    }))),
    source: rec.sources?.[0]?.code,
  }));
}

Fields read: recordType, currentStatus, matchConfidence, cases[].charges[] (description, statute, severity, disposition), sources[].code. Reference: POST /v1/criminal/search; meanings: Understanding charges.

Integrate criminal search into a Django backend

# screening/services.py — Django 4+/5. Settings: OFFENDERSEARCH_API_KEY, OFFENDERSEARCH_BASE
import time, requests
from django.conf import settings

H = {"X-API-Key": settings.OFFENDERSEARCH_API_KEY, "Content-Type": "application/json"}
BASE = getattr(settings, "OFFENDERSEARCH_BASE", "https://api.offendersearch.app")

class ScreeningError(Exception):
    pass

def criminal_search(*, last_name: str, first_name: str = "", dob: str | None = None, state: str | None = None) -> dict:
    query = {"lastName": last_name}
    if first_name: query["firstName"] = first_name
    if dob: query["dob"] = dob
    if state: query["state"] = state
    for attempt in range(4):
        r = requests.post(f"{BASE}/v1/criminal/search", headers=H, json={"query": query, "perPage": 50}, 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
        if r.status_code == 402: raise ScreeningError("trial used up or no active subscription")  # body: {"detail": ...}
        if r.status_code == 422: raise ScreeningError(f"refused: {r.json()['error']}")  # {"error": {"code", "message"}}
        if r.status_code == 202:  # more than two live sources: the job runs in the background

Build an admissions screening workflow

The complete admissions workflow — both searches, outcome classification, audit record, HIPAA notes — is the skilled nursing guide. Its Python and JavaScript modules are production-shaped and run as written.

Configure monitoring notifications

# Create a person watch delivered by webhook (also by email if you add channels.email)
curl -X POST https://api.offendersearch.app/v1/monitors \
  -H "X-API-Key: $OFFENDERSEARCH_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "product": "sex-offender",
    "type": "person",
    "label": "Jordan Rivera — resident 48213",
    "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" },
    "channels": { "webhookUrl": "https://example.com/hooks/offendersearch?token=YOUR_SECRET" }
  }'

# A monitor watches one dataset: repeat with "product": "criminal" to watch criminal records too.

# Read alert history
curl https://api.offendersearch.app/v1/monitors/MONITOR_ID/alerts -H "X-API-Key: $OFFENDERSEARCH_API_KEY"

Exact request fields, the Monitor object and alert events are in the Monitoring reference; delivery, signing status and retries in Delivery. Put a secret token in your webhook URL and verify it on receipt.

Integrate background screening into an existing SaaS application

  1. Add OFFENDERSEARCH_API_KEY and OFFENDERSEARCH_BASE to your secret configuration; point the base at the sandbox in non-production.
  2. Wrap the two searches as a service module (the Python or Node recipe above) with the retry policy and outcome classification.
  3. Send subjectId (your person id) and clientId (your tenant id) on every search so clearances stay per tenant. Clearances.
  4. Store searchId, status, counts and sourceStatus[] with your subject; render POST /v1/report on demand.
  5. Reconcile usage from meterEvents[] where a response carries it (criminal searches, live sources, documents) and from Dashboard → Usage for Regular Sex Offender searches, which report no meterEvents.
  6. Add a Monitoring person watch per active subject and a webhook receiver; cancel on offboarding.
  7. If your product makes employment, housing, credit or insurance decisions, stop here and read the FCRA Partner API: that is the regulated path.