Creating your first monitor
One POST creates a standing watch. This guide takes you from a test key to a delivered alert in four calls.
What you’ll build
A monitor watches one dataset for one predicate and tells you when the answer changes. You will create a sex-offender person monitor on the sandbox, receive its first alert at your webhook, and read that alert back from the API. The same calls, with a live key and the live host, create a production monitor that is evaluated once a day from then on.
This is the job for anyone who needs to know *after* a check, not only at the time of it: a resident screened on admission, a volunteer on a roster, a radius around a school. The result is a Monitor object with status active and, over time, a history of MonitorAlert objects.
Before you start
- A test key (
os_test_…) from your dashboard. Send it inX-API-Keyagainsthttps://sandbox.offendersearch.app. A test key is refused on the live host with a401that names the sandbox. - Monitoring is a product on your existing account, not a separate credential; on the live host it needs an active subscription or an active free trial (
402otherwise). - Sandbox names to use: Exact Match born 1980-01-01 (one
dob_matchalert, jurisdictionOH), Clear Visitor (no alert). The full list is on the sandbox page. - A public
httpsURL that can receive a POST, if you want to see the webhook arrive. A request-inspection service or your staging endpoint both work.
product (sex-offender or criminal) with one type (person or location). Watching the same person in both datasets is two monitors and two charges. There is no combined monitor and no products[] array.Step by step
- Choose the product and the predicate.
product: "sex-offender"withtype: "person"for this guide.type: "person"requires apersonobject with at leastlastName;type: "location"requires alocationobject withlat+lngor a ZIP / "City, ST"address. - Choose the channels.
channelsmust carry at least one email address (email, or several inemails[]) or awebhookUrl. Both at once is fine. Put a secret token in your webhook URL — deliveries are not signed today. - Create the monitor.
POST /v1/monitors. A201returns theMonitorobject; it isactiveimmediately. - Receive the first alert. On the sandbox the monitor is evaluated once, at creation, and every match is filed as a
newalert and POSTed to yourwebhookUrlright away. In production the first evaluation runs in the next daily cycle and reports every current match asnew. - Read the alert back.
GET /v1/monitors/{monitorId}returns the monitor with its ten most recent alerts inrecentAlerts. This is the record to reconcile against.
Create the monitor
curl -X POST https://sandbox.offendersearch.app/v1/monitors \
-H "X-API-Key: $OFFENDERSEARCH_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"product": "sex-offender",
"type": "person",
"person": { "firstName": "Exact", "lastName": "Match", "dob": "1980-01-01" },
"label": "Exact Match — first monitor",
"channels": {
"email": "alerts@your-app.example.com",
"webhookUrl": "https://your-app.example.com/hooks/offendersearch?token=YOUR_SECRET"
}
}'{
"id": "mon_b8f40d7afb9dc0d8",
"product": "sex-offender",
"type": "person",
"minConfidence": null,
"label": "Exact Match — first monitor",
"status": "active",
"channels": {
"emails": ["alerts@your-app.example.com"],
"webhookUrl": "https://your-app.example.com/hooks/offendersearch?token=YOUR_SECRET"
},
"createdAt": "2026-10-25T14:02:11.418223+00:00",
"canceledAt": null,
"person": { "firstName": "Exact", "lastName": "Match", "dob": "1980-01-01" },
"sandbox": true
}Read id (mon_ + 16 hex) and store it with the thing you are watching — it is the key for every later call. channels.email and channels.emails come back merged as channels.emails; webhookUrl is absent when you set none. minConfidence is null because you left it unset, which means the default floor (year_match with a dob). sandbox: true appears on every sandbox body.
Receive the first alert
Within moments the sandbox POSTs this to your webhookUrl, exactly once, in the production payload. Read event first, then detail.matchLabel, then detail.
{
"type": "monitor.alert",
"event": "new",
"alertId": "hit_7fa176c8889c7569",
"monitorId": "mon_b8f40d7afb9dc0d8",
"monitorLabel": "Exact Match — first monitor",
"product": "sex-offender",
"detail": {
"name": "EXACT MATCH",
"jurisdiction": "OH",
"locality": "Sampleville",
"matchLabel": "dob_match",
"matchConfidence": 1.0,
"personRef": "prs_3c9e1f0a7b2d4e6f8a1b",
"recordId": "rec_ab12cd34ef56ab78cd90"
},
"occurredAt": "2026-10-25T14:02:11.602114+00:00",
"sentAt": "2026-10-25T14:02:12.044901+00:00"
}Read the alert back
curl https://sandbox.offendersearch.app/v1/monitors/mon_b8f40d7afb9dc0d8 \
-H "X-API-Key: $OFFENDERSEARCH_TEST_KEY"{
"id": "mon_b8f40d7afb9dc0d8",
"product": "sex-offender",
"type": "person",
"status": "active",
"person": { "firstName": "Exact", "lastName": "Match", "dob": "1980-01-01" },
"recentAlerts": [
{
"id": "hit_7fa176c8889c7569",
"eventType": "new",
"matchLabel": "dob_match",
"record": {
"name": "EXACT MATCH", "jurisdiction": "OH", "locality": "Sampleville",
"matchLabel": "dob_match", "matchConfidence": 1.0,
"personRef": "prs_3c9e1f0a7b2d4e6f8a1b", "recordId": "rec_ab12cd34ef56ab78cd90"
},
"firstSeenAt": "2026-10-25T14:02:11.602114+00:00",
"occurredAt": "2026-10-25T14:02:11.602114+00:00",
"notifiedAt": "2026-10-25T14:02:12.044901+00:00"
}
],
"sandbox": true
}The webhook’s alertId is the alert’s id here, and the webhook’s detail is the alert’s record. notifiedAt is set because the delivery succeeded.
Reading the result
| Field | Values | What it decides |
|---|---|---|
| HTTP status | 201, 200, 401, 402, 422 | 201 created a monitor. 200 is only the criminal + location acknowledgement (status: "not_yet_available") — no monitor exists and nothing is billed. Never treat a 200 as success here. |
status | active, canceled | Whether the monitor is being evaluated. A new monitor is always active. |
channels.emails, channels.webhookUrl | addresses; https URL | Where alerts go. Confirm the webhook URL round-tripped with your token intact. |
recentAlerts[].eventType | new, changed, removed | What happened to the person. The first evaluation reports every current match as new. |
recentAlerts[].matchLabel | dob_match, year_match, age_match, name_match, in_radius | How strongly identity agreed. dob_match is a full-date agreement; name_match is the name alone. |
record.personRef | prs_ + 20 hex | The person this alert is about within that registry — the value to reconcile on next time. |
Edge cases and pitfalls
- No channel —
422 {"detail": "channels must include at least one email or a webhookUrl."}. The monitor is not created. - Street address without coordinates on a location monitor —
422; only a ZIP or "City, ST"addressis placed on its own. Sendlatandlngwith a street address. - `product: "criminal"` + `type: "location"` —
200 {"status": "not_yet_available", "message": "…"}. Branch onstatusbefore you store anid; there is none. - A different date of birth — sandbox Exact Match with
dob: "1990-05-05"creates the monitor (201) but files no alert: the date contradicts the record. Production behaves the same way, so a wrongdobis silent, not an error. - A name-only monitor — sandbox Name Only (no
dob) files threename_matchalerts, one per same-named registrant. Expect that volume on common names in production. - No alert at all —
201and an emptyrecentAlertsis the normal result when nobody matches (sandbox Clear Visitor). In production, the first evaluation is in the next daily cycle, so an empty list right after creation is expected. - Sandbox state — sandbox monitors and alerts live in memory and may be reset when the sandbox is redeployed; the sandbox sends no email, only the webhook.
Production checklist
- Switch to
https://api.offendersearch.appand a live key; store everymon_id against your own record. - Treat only
201as "monitor created"; handle200 not_yet_available,402and422explicitly. - Send a
dobwhenever you have one, and setlabelto something your operators recognise — it is echoed on every webhook asmonitorLabel. - Verify the token in your webhook URL and answer
2xxwithin 10 seconds; reconcile againstGET /v1/monitors/{monitorId}/alerts. - Alerts are not a consumer report and may not be used for any FCRA-covered decision.