Monitoring API · v1.0.0

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 in X-API-Key against https://sandbox.offendersearch.app. A test key is refused on the live host with a 401 that 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 (402 otherwise).
  • Sandbox names to use: Exact Match born 1980-01-01 (one dob_match alert, jurisdiction OH), Clear Visitor (no alert). The full list is on the sandbox page.
  • A public https URL that can receive a POST, if you want to see the webhook arrive. A request-inspection service or your staging endpoint both work.
One product, one predicate. A monitor watches one 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

  1. Choose the product and the predicate. product: "sex-offender" with type: "person" for this guide. type: "person" requires a person object with at least lastName; type: "location" requires a location object with lat + lng or a ZIP / "City, ST" address.
  2. Choose the channels. channels must carry at least one email address (email, or several in emails[]) or a webhookUrl. Both at once is fine. Put a secret token in your webhook URL — deliveries are not signed today.
  3. Create the monitor. POST /v1/monitors. A 201 returns the Monitor object; it is active immediately.
  4. Receive the first alert. On the sandbox the monitor is evaluated once, at creation, and every match is filed as a new alert and POSTed to your webhookUrl right away. In production the first evaluation runs in the next daily cycle and reports every current match as new.
  5. Read the alert back. GET /v1/monitors/{monitorId} returns the monitor with its ten most recent alerts in recentAlerts. 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"
    }
  }'
201 Created
{
  "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.

POST to your webhookUrl — type monitor.alert
{
  "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"
200 — the Monitor with recentAlerts
{
  "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

FieldValuesWhat it decides
HTTP status201, 200, 401, 402, 422201 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.
statusactive, canceledWhether the monitor is being evaluated. A new monitor is always active.
channels.emails, channels.webhookUrladdresses; https URLWhere alerts go. Confirm the webhook URL round-tripped with your token intact.
recentAlerts[].eventTypenew, changed, removedWhat happened to the person. The first evaluation reports every current match as new.
recentAlerts[].matchLabeldob_match, year_match, age_match, name_match, in_radiusHow strongly identity agreed. dob_match is a full-date agreement; name_match is the name alone.
record.personRefprs_ + 20 hexThe 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" address is placed on its own. Send lat and lng with a street address.
  • `product: "criminal"` + `type: "location"` — 200 {"status": "not_yet_available", "message": "…"}. Branch on status before you store an id; 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 wrong dob is silent, not an error.
  • A name-only monitor — sandbox Name Only (no dob) files three name_match alerts, one per same-named registrant. Expect that volume on common names in production.
  • No alert at all — 201 and an empty recentAlerts is 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.app and a live key; store every mon_ id against your own record.
  • Treat only 201 as "monitor created"; handle 200 not_yet_available, 402 and 422 explicitly.
  • Send a dob whenever you have one, and set label to something your operators recognise — it is echoed on every webhook as monitorLabel.
  • Verify the token in your webhook URL and answer 2xx within 10 seconds; reconcile against GET /v1/monitors/{monitorId}/alerts.
  • Alerts are not a consumer report and may not be used for any FCRA-covered decision.

Product overview