How to build an offender alert system that does not cry wolf
Anyone can call a search endpoint on a cron. The hard part is the second run — deciding what counts as new, what counts as changed, and what you must never page a human about twice. This is the part most guides skip.
How a sex offender alert system is structured
Three moving parts: a roster of people you are monitoring, a scheduled search per person, and a diff against what you saw last time. The API supplies the middle one. The other two are yours, and they are where alert quality is won or lost.
Run searches on the Weekly freshness tier for routine monitoring and reserve Daily for the population where currency changes the decision. Freshness is chosen per call, so a single roster can mix both without provisioning two keys.
One monitored person, one call
curl https://api.offendersearch.app/v1/search \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": { "firstName": "John", "lastName": "Doe", "dob": "1980-04-12" },
"freshness": "weekly"
}'Store a fingerprint of each record, not the whole JSON
The naive implementation stores last night's JSON and diffs it whole. It will alert every night forever, because fields that are not about the person still move — a geocoder refines a coordinate, a registry rewrites a description.
Store a fingerprint of the facts you actually care about, keyed on the record identity. Then an alert fires when the fingerprint changes, which is a decision a human recognises: this person moved, this designation changed, this person is newly on the registry.
A fingerprint worth alerting on
import hashlib, json
WATCHED = ("recordId", "housingStatus", "flags", "offense", "addresses")
def fingerprint(rec: dict) -> str:
"""Hash only what a reviewer would consider a real change."""
subset = {k: rec.get(k) for k in WATCHED}
# Addresses: keep the place, drop derived coordinates that drift on re-geocode.
subset["addresses"] = [
{k: a.get(k) for k in ("type", "line1", "city", "state", "zipcode")}
for a in (rec.get("addresses") or [])
]
blob = json.dumps(subset, sort_keys=True, default=str)
return hashlib.sha256(blob.encode()).hexdigest()The part people lose a week to
Do not fingerprint lastCheckedAt. It moves whenever a record is re-verified — by design; that is what it is for — so including it turns your fingerprint into a clock and every run into an alert.
Decide what counts as a match before you alert on one
A search for a common surname returns many people. If you alert on any hit, your reviewers will stop reading the alerts within a week, and the system is then worse than nothing because it looks like it is working.
Every record carries matchConfidence and matchBasis. Use the basis, not just the score: a hit matched on an exact date of birth is a different kind of evidence from one matched on a birth year, which is different again from a name-only hit with no date at all. Set your alert threshold on the basis you are willing to put in front of a person.
- Page a human on an exact-DOB match
- Queue a birth-year match for batch review
- Log a name-only match, do not page anyone
- Record matchBasis on the alert so the reviewer sees why it fired
Handle unreachable registries without reporting a false clear
Every response reports per-source status. A search that reached 57 of 58 registries is not the same answer as one that reached all 58, and the difference matters most in exactly the case an alerting system exists for.
Treat an incomplete search as unknown, not as clear. Re-queue it. The failure that destroys trust in a monitoring system is not a missed alert — it is a clean report that was actually a network error.
The part people lose a week to
This is the single most common bug in home-grown monitoring: an empty result set is read as "nobody matched" when it sometimes means "nothing answered". Check the per-source status on every response before you record a negative.
Build the diff yourself, or use the Monitoring API
Everything above is what you build when you run searches on a schedule. If you would rather not own the roster, the scheduler and the diff, the Monitoring API does that part: create a person monitor (a name and date of birth) or a location monitor (an address or coordinates and a radius), and it emits one alert per change — new, changed or removed — by email or by signed webhook.
A removal is reported too, because disappearance is data: a person who leaves a registry, or a registrant who moves out of the radius, is a change a reviewer may need to know about. Person monitoring is $3 a month and location monitoring is $10 a month; sex offender and criminal person monitors are separate, so one person across both datasets is two monitors.
A person monitor, alerting by email
curl https://api.offendersearch.app/v1/monitors \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "person",
"person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" },
"channels": { "email": "alerts@example.com" }
}'The part people lose a week to
Verify the webhook signature before you act on a payload, and make your handler idempotent: deliveries are retried until your endpoint acknowledges them, so the same alert can arrive more than once.
Route each offender alert to the right reviewer
An alert is only as good as what happens next. Route on match basis and alert type: a new exact-DOB match on a person in an active role goes to a trust-and-safety reviewer the same day; a changed address on an already-reviewed person goes to a lower-priority queue; a removal closes out an open case.
Put everything the reviewer needs on the alert itself — the record URL at the registry, the match basis, the previous and current values of the field that changed — so the decision does not start with a second search. Track time-to-review per queue; an alert queue nobody clears is the same failure as no alerts.
- New + exact-DOB match → same-day human review
- Changed field on a reviewed person → standard queue, show old and new value
- Removed → close or annotate the open case
- Every alert carries recordUrl and matchBasis
Close the loop with a verification report
When an alert leads to a decision, that decision may be reviewed months later by someone who was not there. Generate the verification report at the moment of the decision and attach it to the case: a timestamped document with a source citation on every record.
This is the difference between "the system flagged them" and a citation to the registry record as it read on the day.
Offender alerts and the FCRA: the legal line
Standard results are not a consumer report; FCRA-covered decisions run through FCRA Partner Access, our separately enabled regulated service.
Build the workflow so a human makes the adverse decision and the system supplies evidence — which is better engineering regardless of the regulation.
Building an offender alert system: common questions
How often should I re-screen a monitored population?
Weekly is the right default for a standing roster and carries no surcharge; Daily is worth the extra cent per call for the subset where currency changes the outcome. Because freshness is chosen per request, you can escalate an individual to Daily without changing anything about your key.
How do I avoid alerting on the same person forever?
Alert on a change in a fingerprint of the fields you care about, not on the presence of a match. Store the fingerprint per monitored person and compare on each run.
What should happen when a registry is unreachable?
Record the run as incomplete and retry. Every response reports which sources answered, so an incomplete search is distinguishable from a clean one — never collapse the two.
Should I build monitoring on scheduled searches or on monitors?
Scheduled searches give you full control of the diff and suit a roster you already manage. Monitors move the scheduling and diffing to us and deliver new, changed and removed alerts by email or signed webhook. Many teams start with monitors and only build their own when they need custom fingerprints.
Can I monitor criminal records as well as the registry?
Person monitoring is live on both datasets, as separate monitors. Location monitoring is live for the sex offender registry; criminal location monitoring is coming soon.
Are monitoring alerts a consumer report?
No. Monitoring alerts are not a consumer report and monitoring is not part of FCRA Partner Access. If an alert will inform a hiring or tenant decision, that decision runs through an FCRA workflow.