# Monitoring API documentation

> Standing watches on a person or a location, from the same account and key as the Sex Offender and Criminal Search APIs. Create a monitor, and receive alerts by email or webhook when a matching record appears, changes or is removed. Evaluated once per day; each monitor is one product, one predicate, one charge.

- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed; see https://offendersearch.app/docs/monitoring/sandbox.md
- **Authentication:** `X-API-Key` request header on every request
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **HTML documentation:** https://offendersearch.app/docs/monitoring
- **Per-section markdown:** https://offendersearch.app/docs/monitoring/{section}.md

Monitoring alerts are not a consumer report, and monitoring is not part of FCRA Partner Access. They may not be used for any FCRA-covered purpose (employment, tenant screening, credit, insurance) — a restriction that matters most here, because a monitor runs unattended. *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.).* Every alert email states this restriction in its footer.

## Contents

- [Quickstart](https://offendersearch.app/docs/monitoring/quickstart.md) — Your first monitor in three languages, and the alert it delivers when a record matches.
- [Access, keys & data handling](https://offendersearch.app/docs/monitoring/authentication.md) — The X-API-Key header, the security posture, and how monitored criteria and an address are stored.
- [Monitoring API sandbox & test keys](https://offendersearch.app/docs/monitoring/sandbox.md) — A test key, the sandbox base URL, person and location monitor scenarios, and a webhook test event. Never billed.
- [The Monitor object](https://offendersearch.app/docs/monitoring/monitor-object.md) — One object per watch — its predicate, its channels, its status — and how to read every field.
- [Person monitoring](https://offendersearch.app/docs/monitoring/person-monitoring.md) — Watch a person by name and DOB, why the DOB matters, and the confidence floor an alert clears.
- [Location monitoring](https://offendersearch.app/docs/monitoring/location-monitoring.md) — Watch a radius around an address, with sex-offender coverage live and criminal records coming.
- [Alerts, events & cadence](https://offendersearch.app/docs/monitoring/alerts.md) — The alert object, the new / changed / removed events, the daily cadence, and de-duplication.
- [Delivery — email & webhooks](https://offendersearch.app/docs/monitoring/delivery.md) — Email to every recipient, an optional webhook, the monitor.alert payload, and the retry rule.
- [Billing — one predicate, one charge](https://offendersearch.app/docs/monitoring/billing.md) — The price matrix, the one-predicate-one-charge rule, and when cancelling ends the charge.
- [Endpoint & error reference](https://offendersearch.app/docs/monitoring/endpoints.md) — All five endpoints in one place — create, list, retrieve, cancel, alerts — and the error shape.
- [Monitoring API reference](https://offendersearch.app/docs/monitoring/api-reference.md) — Every monitoring endpoint and object, each linked to its Request and Response tables.

---

# Quickstart

> Create your first person monitor in cURL, Node or Python, receive the alert a matching record triggers, and read the five fields on every alert payload.

HTML: https://offendersearch.app/docs/monitoring/quickstart · Markdown: https://offendersearch.app/docs/monitoring/quickstart.md

## Authenticate

Every request carries your secret key in the `X-API-Key` header — the same key you use for the Sex Offender API and the Criminal Search API. Monitoring is a product on your existing account, not a separate credential.

```bash
export OFFENDERSEARCH_BASE=https://api.offendersearch.app
export OFFENDERSEARCH_KEY="os_live_…"
```

## Create your first monitor

A monitor is a standing watch. This one watches the sex-offender dataset for one person by name and date of birth, and delivers an alert whenever a matching record appears, changes, or comes off a registry. `product`, `type`, the predicate and at least one channel are required.

```bash
curl -X POST "$OFFENDERSEARCH_BASE/v1/monitors" \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "product": "sex-offender",
        "type": "person",
        "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" },
        "minConfidence": "dob_match",
        "label": "Jordan Rivera — SO watch",
        "channels": { "email": "alerts@example.com" }
      }'
```

The response is the [Monitor object](https://offendersearch.app/docs/monitoring/monitor-object.md) with `201 Created`. It is `active` immediately and evaluated once per day from then on.

```json
{
  "id": "mon_b8f40d7afb9dc0d8",
  "product": "sex-offender",
  "type": "person",
  "minConfidence": "dob_match",
  "label": "Jordan Rivera — SO watch",
  "status": "active",
  "channels": { "emails": ["alerts@example.com"] },
  "createdAt": "2026-09-05T14:02:11+00:00",
  "canceledAt": null,
  "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" }
}
```

## Receive an alert

When the daily evaluation finds a matching record, an alert is delivered to every channel on the monitor. This is the JSON body POSTed to a webhook — the [monitor.alert payload](https://offendersearch.app/docs/monitoring/delivery#object-webhookdelivery). Its `detail` is the same record summary the alert-history endpoint returns as `record`.

```json
{
  "type": "monitor.alert",
  "event": "new",
  "alertId": "hit_7fa176c8889c7569",
  "monitorId": "mon_b8f40d7afb9dc0d8",
  "monitorLabel": "Jordan Rivera — SO watch",
  "product": "sex-offender",
  "detail": {
    "name": "JORDAN RIVERA",
    "jurisdiction": "TX",
    "locality": "Houston",
    "matchLabel": "dob_match",
    "matchConfidence": 1.0,
    "personRef": "prs_3c9e1f0a7b2d4e6f8a1b",
    "recordId": "rec_ab12cd34ef56gh78ij90"
  },
  "occurredAt": "2026-09-06T12:00:41+00:00",
  "sentAt": "2026-09-06T12:01:03+00:00"
}
```

## Read the alert, in this order, every time

1. **`event`** — `new` (a matching record newly appeared), `changed` (a monitored record’s details changed), or `removed` (it no longer matches). It tells you what happened before you read the record.
2. **`detail.matchLabel`** — how strongly identity matched: `dob_match` is the full date of birth, `year_match` the birth year only, `age_match` a published age, `name_match` the name alone. A monitor only alerts at or above its `minConfidence` floor.
3. **`detail`** — the matched record, summarised: the name, a neutral jurisdiction code, the city, and the `personRef` to recognise the person by next time. On a `removed` event it carries only `event` and `jurisdiction`.
4. **`monitorId`** — which monitor fired, so one webhook endpoint can serve many monitors.
5. **`occurredAt`** — when this record first matched the monitor, in ISO-8601.

## Where to go next

- [Person monitoring](https://offendersearch.app/docs/monitoring/person-monitoring.md) — name + DOB, and why a DOB matters.
- [Location monitoring](https://offendersearch.app/docs/monitoring/location-monitoring.md) — watch a radius around an address.
- [Alerts](https://offendersearch.app/docs/monitoring/alerts.md) — the event types, the daily cadence and de-duplication.
- [Delivery](https://offendersearch.app/docs/monitoring/delivery.md) — email, webhooks and the retry rule.
- [Billing](https://offendersearch.app/docs/monitoring/billing.md) — the price matrix and the one-predicate-one-charge rule.

## Test the monitoring API in the sandbox

Create person and location monitors on the sandbox with a test key (os_test_…). A sandbox monitor is checked once, at creation: each match is filed as a new alert and sent to your webhook, and a sandbox-only test event sends another.

- **Sandbox base URL:** `https://sandbox.offendersearch.app`. Not `https://api.offendersearch.app`, which takes live keys only.
- **Test key:** in the dashboard, Developers → API keys (https://offendersearch.app/dashboard/keys) → **Create test key**. Test keys begin `os_test_`; send yours in the `X-API-Key` header.

POST /v1/monitors

```bash
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"
      },
      "channels": {
        "webhookUrl": "https://your-app.example.com/hooks/offendersearch"
      }
    }'
```

- Every test name: [Monitoring API sandbox](https://offendersearch.app/docs/monitoring/sandbox#monitoring)
- Postman collection: https://offendersearch.app/downloads/offendersearch-sandbox.postman_collection.json
- Monitoring API sandbox: https://offendersearch.app/docs/monitoring/sandbox.md
- Run any scenario from the browser: the HTML page's "Try it here" explorer (sandbox host and `os_test_` keys only, never billed).

---

# Access, keys & data handling

> Authenticate with the X-API-Key header on every monitoring call, and see how a monitor stores its criteria: encrypted at rest and isolated to your account.

HTML: https://offendersearch.app/docs/monitoring/authentication · Markdown: https://offendersearch.app/docs/monitoring/authentication.md

## The X-API-Key header

Authenticate every `/v1/monitors*` request with your secret key in the `X-API-Key` header. Keys are created, named, rotated and revoked from the dashboard, and a key's secret is shown in full only once, at creation. It is the **same** key you already use for the Sex Offender API and the Criminal Search API.

```bash
curl "https://api.offendersearch.app/v1/monitors" \
  -H "X-API-Key: os_live_…"
```

A missing or invalid key returns `401` with `{"detail": "Provide a valid X-API-Key or session token."}` — the error envelope every monitoring endpoint uses.

## Monitoring is available on every account

There is no product to enable and no entitlement to request. Any secret API key on an account with an active subscription or free trial can create, list, retrieve and cancel monitors and read their alert history immediately; without either, the call is a `402`. Monitors belong to the account, not to the key that created them, so every key on the account lists and manages the same monitors.

**Rotation.** Issue a second key, deploy it, then revoke the first — both are valid at once, so rotation needs no downtime window.

## Security posture

- **Encryption.** TLS in transit, AES-256 at rest for stored monitors and alerts.
- **Per-key hashing.** Secrets are hashed at rest — a database read never exposes a usable key.
- **Tenant isolation.** Monitors, alerts and channels are scoped per account; one customer can never read another’s.
- **Least privilege.** A key authenticates you and nothing more; there are no per-key feature flags.
- **Attestations.** A Business Associate Agreement (BAA) is available to eligible enterprise accounts.

## How a monitor stores its criteria

A monitor persists the criteria you register so it can be evaluated day after day. The monitored person’s name and date of birth — and, for a location monitor, the subscriber’s own address (or coordinates and radius) — are stored **encrypted at rest** and isolated to your account. They are used to evaluate the monitor and to name it in its own alerts. They are never shared between accounts and never surfaced in another customer’s data.

## Legal

Monitoring alerts are not a consumer report, and monitoring is not part of FCRA Partner Access. You may not use monitoring for employment, tenant screening, credit, insurance, or any other FCRA-covered purpose — a restriction that is especially load-bearing here, because a monitor runs unattended and could otherwise feed an automated adverse decision. *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.).* Every alert email states this restriction in its footer, and there is no parameter that removes it.

---

# Monitoring API sandbox & test keys

> Test the Monitoring API in the sandbox with an os_test_ key: person and location monitors, alerts sent to your webhook at once, and a test event. Never billed.

HTML: https://offendersearch.app/docs/monitoring/sandbox · Markdown: https://offendersearch.app/docs/monitoring/sandbox.md

## Monitoring API test keys: create one in the dashboard

The monitoring sandbox is the Monitoring API on a separate server: create, list, read and cancel person and location monitors, read their alerts, and receive them at your webhook, with the same paths and payloads as live. It matches against a fixed set of made-up people, and nothing you send is billed.

1. In the dashboard, open [Developers → API keys](https://offendersearch.app/dashboard/keys).
2. Choose **Create test key**. Test keys are for business accounts and need no subscription.
3. Copy the key. Test keys begin `os_test_`. Like a live key, the full secret is shown once, so put it in your secrets store straight away.

One test key works on every API’s sandbox. Each key in the list is labelled **Test · Sandbox** or **Live**, with the base URL it works on. Send it in the same `X-API-Key` header the live API uses. An account holds up to 3 active test keys and can create 10 a day. A revoked test key stops working on the sandbox within a few minutes.

## Monitoring API sandbox base URL vs live

|  | Sandbox | Production |
| --- | --- | --- |
| Base URL | `https://sandbox.offendersearch.app` | `https://api.offendersearch.app` |
| Key prefix | `os_test_` | `os_live_` |
| Data | The fixed test people and the sample point listed below | Live sex offender and criminal records |
| Billing | Never billed, never recorded to usage | Your plan’s prices |
| `sandbox` field | `true` on every response | Absent |
| `X-Offendersearch-Environment` | `sandbox` on every response | Absent |

Each key works on its own server only, so a mixed-up configuration is caught on the first request rather than after a release. A test key sent to the live API, or a live key sent to the sandbox, is refused with `401` and a `detail` message that says so:

401 — a test key sent to the live API

```json
{
  "detail": "This is a test key (os_test_…). Test keys work only on the sandbox at https://sandbox.offendersearch.app, which answers with sample data. To search real records, send a live key (os_live_…) to https://api.offendersearch.app."
}
```

401 — a live key sent to the sandbox

```json
{
  "detail": "This is a live key. The sandbox at https://sandbox.offendersearch.app accepts test keys (os_test_…) only — create one in the dashboard under Developers → API keys. Send live keys to https://api.offendersearch.app."
}
```

A revoked test key gets `401` with: “This test key has been revoked. Create a new test key in the dashboard under Developers → API keys.”

Each test key can send 60 requests a minute and 2,000 a day, and one IP address 120 a minute, across every API on the sandbox. Past a limit the sandbox answers `429` with a `Retry-After` header. See [Sandbox rate limits](https://offendersearch.app/docs/sandbox.md#rate-limits).

## Monitoring API sandbox: person and location monitors

`POST /v1/monitors` and the rest of the [Monitoring API](https://offendersearch.app/docs/monitoring.md) run on the sandbox. A sandbox monitor is checked once, when you create it, against the test people: each match is filed at once as a `new` alert and sent to `channels.webhookUrl` one time, in the production payload. The sandbox sends no email. Nothing is billed.

| Send | What comes back |
| --- | --- |
| `sex-offender · person`, `firstName: Exact · lastName: Match · dob: 1980-01-01`, `channels.webhookUrl` | **Person monitor, date of birth verified.** 201 Created. One `new` alert at once (matchLabel dob_match, jurisdiction OH), POSTed to the webhook once. |
| `sex-offender · person`, `firstName: Name · lastName: Only`, `channels.email` | **Person monitor, name only.** 201 Created. Three `new` alerts (matchLabel name_match), one per sample registrant named Name Only. |
| `sex-offender · person`, `firstName: Multi · lastName: State · dob: 1972-03-14`, `channels.email` | **Person monitor, one person on two registries.** 201 Created. Two `new` alerts — one per registry that holds the person (FL and GA), as production files them. |
| `sex-offender · person`, `firstName: Clear · lastName: Visitor`, `channels.email` | **Person monitor, nobody matches.** 201 Created. No alerts. |
| `sex-offender · person`, `firstName: Exact · lastName: Match · dob: 1990-05-05`, `channels.email` | **Person monitor, different date of birth.** 201 Created. No alerts: the date of birth contradicts the record. |
| `sex-offender · location`, `lat 39.9612, lng -82.9988, 1 mi`, `channels.webhookUrl` | **Location monitor around the sample point.** 201 Created. One `new` alert (matchLabel in_radius) for the sample registrant Exact Match, POSTed to the webhook once. |
| `sex-offender · location`, `lat 34.0522, lng -118.2437, 3 mi`, `channels.email` | **Location monitor anywhere else.** 201 Created. No alerts. |
| `criminal · person`, `firstName: Felony · lastName: Record · dob: 1975-06-15`, `channels.email` | **Criminal person monitor.** 201 Created. One `new` alert (matchLabel dob_match, externalId SBX-CR-2015-000123). |
| `criminal · location`, `lat 39.9612, lng -82.9988, 1 mi`, `channels.email` | **Criminal location monitor.** 200 with status not_yet_available, exactly as production: nothing is created. |
| `POST /v1/sandbox/monitors/{monitorId}/test-event` | **Send a test event (sandbox only).** Files one more `new` alert on the monitor (its own first match, else the sample registrant) and POSTs it to the monitor's webhook now. Returns the alert and webhookDelivered. |

POST /v1/monitors — sandbox

```bash
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"
      },
      "channels": {
        "webhookUrl": "https://your-app.example.com/hooks/offendersearch"
      }
    }'
```

## Monitoring webhook test event in the sandbox

To test your webhook handler again without creating a new monitor, ask the sandbox for one more alert on a monitor you created. This endpoint exists only on the sandbox. The alert arrives in the same payload a live alert uses, so your handler can check the secret token in your `webhookUrl` and read `monitorId` exactly as it will in production. See [Delivery](https://offendersearch.app/docs/monitoring/delivery.md).

Send a test event — sandbox only

```bash
curl -X POST "https://sandbox.offendersearch.app/v1/sandbox/monitors/{monitorId}/test-event" \
  -H "X-API-Key: $OFFENDERSEARCH_TEST_KEY"
```

## Monitoring API sandbox examples in cURL, Node and Python

Person monitor, date of birth verified: the monitor is checked once at creation, and its alert is sent to your webhook. Export your test key as `OFFENDERSEARCH_TEST_KEY` first.

POST /v1/monitors — sandbox — cURL

```bash
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"
      },
      "channels": {
        "webhookUrl": "https://your-app.example.com/hooks/offendersearch"
      }
    }'
```

POST /v1/monitors — sandbox — Node

```javascript
// Node 18+ — fetch is built in.
const BASE = process.env.OFFENDERSEARCH_BASE ?? 'https://sandbox.offendersearch.app';

const res = await fetch(`${BASE}/v1/monitors`, {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.OFFENDERSEARCH_TEST_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "product": "sex-offender",
    "type": "person",
    "person": {
      "firstName": "Exact",
      "lastName": "Match",
      "dob": "1980-01-01"
    },
    "channels": {
      "webhookUrl": "https://your-app.example.com/hooks/offendersearch"
    }
  }),
});
const body = await res.json();

console.log(res.headers.get('X-Offendersearch-Environment')); // "sandbox"
console.log(body.sandbox);  // true
console.log(body.id);       // keep it for the test event
```

POST /v1/monitors — sandbox — Python

```python
import os
import requests

BASE = os.environ.get("OFFENDERSEARCH_BASE", "https://sandbox.offendersearch.app")

r = requests.post(
    f"{BASE}/v1/monitors",
    headers={"X-API-Key": os.environ["OFFENDERSEARCH_TEST_KEY"]},
    json={
        "product": "sex-offender",
        "type": "person",
        "person": {
            "firstName": "Exact",
            "lastName": "Match",
            "dob": "1980-01-01",
        },
        "channels": {
            "webhookUrl": "https://your-app.example.com/hooks/offendersearch",
        },
    },
    timeout=30,
)
body = r.json()

print(r.headers["X-Offendersearch-Environment"])  # sandbox
print(body["sandbox"])  # True
print(body["id"])       # keep it for the test event
```

## Postman collection for the monitoring API sandbox

One collection covers every API’s sandbox, with a folder per API, ready to import into Postman or Insomnia: https://offendersearch.app/downloads/offendersearch-sandbox.postman_collection.json. The **Monitoring API** folder has one request for every scenario on this page. Set `{{apiKey}}` to your test key; `{{baseUrl}}` is preset to the sandbox. Ids from earlier responses go in `{{monitorId}}`.

## Move monitoring from the sandbox to production

The sandbox and the live API differ only in the base URL and the key. Keep both in configuration, not in code, and going live is a configuration change:

.env

```bash
# Sandbox
OFFENDERSEARCH_BASE=https://sandbox.offendersearch.app
OFFENDERSEARCH_KEY=os_test_…

# Production — change these two values and nothing else
OFFENDERSEARCH_BASE=https://api.offendersearch.app
OFFENDERSEARCH_KEY=os_live_…
```

- Create a live key on the same [Developers → API keys](https://offendersearch.app/dashboard/keys) page.
- Remove any branch that reads `sandbox`. The field does not exist on live.
- Remove the sandbox-only test-event call. It does not exist on live.
- Live monitors are billed monthly, each dataset separately; see [Billing](https://offendersearch.app/docs/monitoring/billing.md).

---

# The Monitor object

> Every field on a monitor: the id, product, type, the person or location predicate, minConfidence, label, status, channels and createdAt, each one explained.

HTML: https://offendersearch.app/docs/monitoring/monitor-object · Markdown: https://offendersearch.app/docs/monitoring/monitor-object.md

## One object per watch

A monitor is the durable record of a standing watch. The same object is returned by `POST /v1/monitors`, by `GET /v1/monitors` (in a list), and by `GET /v1/monitors/{id}` (with `recentAlerts` attached). A person monitor carries a `person` key and a location monitor a `location` key, never both; every other key is on every monitor.

```json
{
  "id": "mon_b8f40d7afb9dc0d8",
  "product": "sex-offender",
  "type": "person",
  "minConfidence": "dob_match",
  "label": "Jordan Rivera — SO watch",
  "status": "active",
  "channels": { "emails": ["alerts@example.com"] },
  "createdAt": "2026-09-05T14:02:11+00:00",
  "canceledAt": null,
  "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" }
}
```

## Fields — every field on the Monitor object

One standing watch, as `POST /v1/monitors`, `GET /v1/monitors` and `GET /v1/monitors/{monitorId}` return it. A person monitor carries `person` and no `location` key; a location monitor carries `location` and no `person` key — read `type`, then the matching object.

Returned or accepted by [`POST /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors), [`GET /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-get-v1-monitors), [`GET /v1/monitors/{monitorId}`](https://offendersearch.app/docs/monitoring/endpoints#reference-get-v1-monitors-id), [`POST /v1/monitors/{monitorId}/webhook-secret`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors-id-webhook-secret).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The monitor id: `mon_` followed by 16 hex characters. Use it in every `/v1/monitors/{monitorId}` path. Example `"mon_b8f40d7afb9dc0d8"`. |
| `product` | `string` — one of `"sex-offender"`, `"criminal"` |  | The dataset this monitor watches. |
| `type` | `string` — one of `"person"`, `"location"` |  | The predicate: which of `person` or `location` this monitor carries. |
| `minConfidence` | `string` or null — one of `"dob_match"`, `"year_match"`, `"age_match"`, `"name_match"`, `"any"` |  | The alert floor exactly as you set it. `null` when you did not set one, in which case the default floor applies (`year_match` with a `dob`, `name_match` without). |
| `label` | `string` or null |  | Your label, or `null` when you did not set one. |
| `status` | `string` — one of `"active"`, `"canceled"` |  | `active`: evaluated and billed. `canceled`: no longer evaluated, and not charged on any invoice prepared after the cancel; the monitor and its alerts stay readable. |
| `channels` | `object` |  | Where alerts are delivered, normalised. |
| `channels.emails` | `array of string` |  | Every recipient address you set (`email` and `emails` merged, de-duplicated). `[]` when the monitor delivers by webhook only. The account owner's email receives the alert email in addition to these. |
| `channels.webhookUrl` | `string (uri)` |  | The webhook endpoint. The key is absent when no webhook is set. |
| `channels.webhookSecret` | `string` |  | The monitor's webhook signing secret (`whsec_…`). **Present only on the `201` that created the monitor and on the response of `POST /v1/monitors/{monitorId}/webhook-secret`** — never on `GET` or list. Store it; every delivery to `webhookUrl` is signed with it. Example `"whsec_…"`. |
| `createdAt` | `string (date-time)` |  | When the monitor was created, ISO-8601 in UTC. Example `"2026-10-14T04:13:16.599893+00:00"`. |
| `canceledAt` | `string (date-time)` or null |  | When the monitor was canceled; `null` while it is `active`. |
| `person` | `object` |  | The person criteria, normalised. Present only on a `person` monitor. |
| `person.firstName` | `string` or null |  | Given name, or `null` when none was sent. |
| `person.lastName` | `string` |  | Surname. |
| `person.dob` | `string (date)` or null |  | Date of birth, `YYYY-MM-DD`, or `null` when none was sent. |
| `location` | `object` |  | The location criteria, normalised. Present only on a `location` monitor. |
| `location.address` | `string` or null |  | The address you sent, or `null`. |
| `location.lat` | `number` or null |  | Latitude of the centre, or `null`. |
| `location.lng` | `number` or null |  | Longitude of the centre, or `null`. |
| `location.radiusMiles` | `number` |  | Radius in miles; `3.0` when you did not send one. |
| `recentAlerts` | array of [Alert](https://offendersearch.app/docs/monitoring/alerts#the-alert-object) |  | The ten most recently observed alerts, newest first. Present only on `GET /v1/monitors/{monitorId}`; `GET /v1/monitors/{monitorId}/alerts` returns up to 200. |

## person and location are mutually exclusive

A monitor carries exactly one predicate. On a `type: "person"` monitor, `person` is present and there is no `location` key; on a `type: "location"` monitor it is the reverse. Read `type` to know which to expect, then read the matching object.

## status and the monitor lifecycle

| `status` | Meaning |
| --- | --- |
| `active` | The monitor is evaluated once per day and is billed on your monthly invoice. |
| `canceled` | You canceled it with `DELETE /v1/monitors/{id}`. It stops being evaluated at once, it is not charged on any invoice prepared after you cancel it, and `canceledAt` records when. It stays readable so its alert history is not lost. |

> A `canceled` monitor is not deleted — `GET /v1/monitors/{id}` still returns it and its alerts. Cancelling is how you stop a watch; there is no separate delete.

## GET /v1/monitors/{id}

Retrieve one monitor together with its ten most recently observed alerts. A monitor is visible only to the account that created it — another account’s `id` returns `404`. Every request and response field is in the [GET /v1/monitors/{id} reference](https://offendersearch.app/docs/monitoring/endpoints#reference-get-v1-monitors-id).

```bash
curl "https://api.offendersearch.app/v1/monitors/mon_b8f40d7afb9dc0d8" \
  -H "X-API-Key: $OFFENDERSEARCH_KEY"
```

```json
{
  "id": "mon_b8f40d7afb9dc0d8",
  "product": "sex-offender",
  "type": "person",
  "minConfidence": "dob_match",
  "label": "Jordan Rivera — SO watch",
  "status": "active",
  "channels": { "emails": ["alerts@example.com"] },
  "createdAt": "2026-09-05T14:02:11+00:00",
  "canceledAt": null,
  "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" },
  "recentAlerts": [
    { "id": "hit_7fa176c8889c7569", "eventType": "new", "matchLabel": "dob_match",
      "record": { "name": "JORDAN RIVERA", "jurisdiction": "TX", "locality": "Houston",
                  "matchLabel": "dob_match", "matchConfidence": 1.0 },
      "firstSeenAt": "2026-09-06T12:00:41+00:00",
      "occurredAt": "2026-09-06T12:00:41+00:00",
      "notifiedAt": "2026-09-06T12:01:03+00:00" }
  ]
}
```

---

# Person monitoring

> Watch one person by name and date of birth across a dataset. Supply a DOB so alerts fire above a confidence floor, and see why a name-only watch is noisier.

HTML: https://offendersearch.app/docs/monitoring/person-monitoring · Markdown: https://offendersearch.app/docs/monitoring/person-monitoring.md

## Watch one person by name and date of birth

A person monitor stands watch over one identity — a name, and, when you supply it, a date of birth — across the dataset named in `product`. Each day it runs the same matcher the search endpoint uses, and when a matching record appears, changes, or stops matching, it delivers an alert.

```json
{
  "product": "sex-offender",
  "type": "person",
  "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" },
  "minConfidence": "dob_match",
  "channels": { "email": "alerts@example.com" }
}
```

## Supply a date of birth

A name on its own matches many people, so a name-only monitor is **noisy** — it will alert on every same-named record in the dataset. A date of birth is the strongest identity verifier, and supplying it is the single most effective way to keep a monitor precise. The matcher uses the same match strength labels as search: a `dob` you provide can produce a `dob_match` (the full date agrees) or, where a record publishes only a birth year, a `year_match`.

> **Recommendation:** always send a `dob`. Reserve a name-only monitor for the rare case where you genuinely want every same-named record and will triage the volume yourself.

Once a person matches, the monitor follows *them*, not the listing. Each alert is keyed on the record’s `personRef` within its registry, so a registry that republishes the listing under a new number leaves the alert thread — and its `id` — unchanged; only a fact about the person (an address, a status, a new booking on a criminal monitor) produces a `changed` event. See [One alert per person](https://offendersearch.app/docs/monitoring/alerts.md#one-alert-per-person).

## minConfidence — the alert floor

A person monitor only fires when identity matches at or above its confidence floor. `minConfidence` sets that floor.

| `minConfidence` | Fires on | Use it when |
| --- | --- | --- |
| `dob_match` | The record’s full date of birth equals the one you registered. | You want the tightest, lowest-noise watch. |
| `year_match` | `dob_match`, plus records that publish only a birth year and agree on it. **The default when you send a `dob`.** | You want records that publish a year but no full date as well. |
| `age_match` | The above, plus records that publish only an age consistent with your date of birth. | You accept an age-only corroboration, at the cost of more alerts. |
| `name_match` | Every record that matches the name and does not contradict your `dob`. **The default without a `dob`.** | You will triage every same-named record yourself. |
| `any` | The default floor: `year_match` with a `dob`, `name_match` without. | You want the default and prefer to say so explicitly. |

Leave `minConfidence` unset and the default applies. Any value outside these five is a `422`. The `matchLabel` on every alert tells you which strength actually fired, so under any floor you can tell a full-date confirmation from a year-only or age-only one.

## The PersonCriteria object — person monitor request fields

Who a person monitor watches: `person` on `POST /v1/monitors`. Returned on the monitor as `person`.

Returned or accepted by [`POST /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `firstName` | `string` or null | optional | Given name. Leading and trailing spaces are trimmed; an empty value is stored as `null` and the monitor matches on `lastName` (and `dob`) alone. Example `"Jordan"`. |
| `lastName` | `string` | required | Surname. Required: a person monitor without it is a `422`. Example `"Rivera"`. |
| `dob` | `string (date)` or null | optional | Date of birth, `YYYY-MM-DD`; any other format is a `422`. With a `dob` the default alert floor is `year_match`; without one it is `name_match`, which alerts on every same-named record. Example `"1988-04-12"`. |

## What a person monitor watches

- **`product: "sex-offender"`** — the sex-offender dataset. **Live.**
- **`product: "criminal"`** — the criminal-records dataset. **Live.**

Watching one person across **both** datasets is two monitors, billed separately — see [Billing](https://offendersearch.app/docs/monitoring/billing.md). Each monitor is one product, one predicate, one charge.

## Not a consumer report

A person monitor runs unattended, which makes the FCRA restriction especially important: its alerts may not be used for employment, tenant screening, credit, insurance, or any other FCRA-covered decision. *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.).* Every alert email carries a not-a-consumer-report notice in its footer.

---

# Location monitoring

> Watch a radius around an address and be alerted as matching records move in or out. Live for sex-offender records; criminal-record location watches are coming.

HTML: https://offendersearch.app/docs/monitoring/location-monitoring · Markdown: https://offendersearch.app/docs/monitoring/location-monitoring.md

## Watch a radius around an address

A location monitor stands watch over a place rather than a person. You give it a centre and a radius, and each day it evaluates which records fall inside that radius — alerting you as records move **in** (`new`), change while inside it (`changed`), or move **out** or come off a registry (`removed`).

```json
{
  "product": "sex-offender",
  "type": "location",
  "location": { "address": "100 Main St, Austin, TX 78701",
                "lat": 30.2711, "lng": -97.7437, "radiusMiles": 1 },
  "channels": { "email": "alerts@example.com" }
}
```

The radius is evaluated around `lat`/`lng`, so send the coordinates. On its own, a ZIP code or "City, ST" is placed at its centre; a street address without coordinates is refused with a 422, so a monitor is never created that cannot be evaluated.

```json
{
  "product": "sex-offender",
  "type": "location",
  "location": { "lat": 30.2711, "lng": -97.7437, "radiusMiles": 2 },
  "channels": { "email": "alerts@example.com", "webhookUrl": "https://example.com/hooks/os" }
}
```

## The LocationCriteria object — location monitor request fields

The centre and radius a location monitor watches: `location` on `POST /v1/monitors`. Returned on the monitor as `location`.

Returned or accepted by [`POST /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `address` | `string` or null | optional | A street address or area, stored with the monitor as you sent it (trimmed). On its own, a ZIP code or "City, ST" is placed at its centre; a street address must come with `lat` and `lng` or the request is a 422. The radius is evaluated around the coordinates. Example `"100 Main St, Austin, TX 78701"`. |
| `lat` | `number` or null | optional | Latitude of the centre, in decimal degrees. Example `30.2711`. |
| `lng` | `number` or null | optional | Longitude of the centre, in decimal degrees. Example `-97.7437`. |
| `radiusMiles` | `number` | optional · default `3` | Radius in miles. Greater than 0 and at most 100; outside that range is a `422`. Example `1`. |

## Coverage

| `product` | `type: "location"` |
| --- | --- |
| `sex-offender` | **Live.** Sex-offender location monitoring is available today. |
| `criminal` | **Coming soon.** See below. |

## Criminal location monitoring is coming soon

Criminal-record location monitoring will arrive **as location coverage across criminal records expands**. It is documented here so you can plan for it, but it is not billable yet. A `POST /v1/monitors` with `product: "criminal"` and `type: "location"` does not error — it returns a `not_yet_available` acknowledgement, so your integration can handle the transition without special-casing an error path.

```json
// POST /v1/monitors  {"product":"criminal","type":"location", …}
{
  "status": "not_yet_available",
  "message": "Location monitoring for criminal records is coming soon — it activates as location coverage across criminal records expands. Person monitoring is available for criminal records today."
}
```

> The response is a `200` acknowledgement, not a `4xx`. No monitor is created and nothing is billed. Treat `status: "not_yet_available"` as a signal to hold that watch until the product is live.

## The address you register

The centre of a location monitor is the subscriber’s own input. It is stored **encrypted at rest** and isolated to your account, and it is used to evaluate proximity each day and to name the monitor in its own alerts. See [Access, keys & data handling](https://offendersearch.app/docs/monitoring/authentication.md).

## Not a consumer report

A location monitor’s alerts are not a consumer report and may not be used for any FCRA-covered decision. *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.).* Every alert email carries a not-a-consumer-report notice in its footer.

---

# Alerts, events & cadence

> The alert object and its three event types — new, changed and removed — plus the once-daily evaluation cadence and how repeat alerts are de-duplicated for you.

HTML: https://offendersearch.app/docs/monitoring/alerts · Markdown: https://offendersearch.app/docs/monitoring/alerts.md

## The alert object

One person's latest event on one monitor, as the alert history and `recentAlerts` return it. There is one alert per monitor and person per registry: when a matched person's record changes again, or a registry republishes it under a new number, the alert keeps the same `id` and (on a change) takes the new `eventType`.

Returned or accepted by [`POST /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors), [`GET /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-get-v1-monitors), [`GET /v1/monitors/{monitorId}`](https://offendersearch.app/docs/monitoring/endpoints#reference-get-v1-monitors-id), [`GET /v1/monitors/{monitorId}/alerts`](https://offendersearch.app/docs/monitoring/alerts#alert-history), [`POST /v1/monitors/{monitorId}/webhook-secret`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors-id-webhook-secret).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  | The alert id: `hit_` followed by 16 hex characters. Stable for one monitor and one person in one registry across events, including a registry renumbering the listing. A webhook delivery carries it as `alertId`. Example `"hit_7fa176c8889c7569"`. |
| `eventType` | `string` — one of `"new"`, `"changed"`, `"removed"` |  | The latest event for this person: `new` (the person newly matches), `changed` (a fact about them changed while matching — an address, an offense field, a status, a new booking), `removed` (they no longer match — no longer listed in that registry, or outside a location monitor's radius). A registry renumbering a listing is not an event. |
| `matchLabel` | `string` or null — one of `"dob_match"`, `"year_match"`, `"age_match"`, `"name_match"`, `"in_radius"` |  | How the record matched: `dob_match`, `year_match`, `age_match` or `name_match` on a person monitor, `in_radius` on a location monitor. `null` on a `removed` event. |
| `record` | [AlertRecord](https://offendersearch.app/docs/monitoring/alerts#object-alertrecord) |  | The matched record, summarised. |
| `firstSeenAt` | `string (date-time)` |  | When this record first matched the monitor. Example `"2026-09-06T12:00:41+00:00"`. |
| `occurredAt` | `string (date-time)` |  | The most recent evaluation that recorded this record for the monitor. Alerts are listed by this field, newest first. Example `"2026-09-06T12:00:41+00:00"`. |
| `notifiedAt` | `string (date-time)` or null |  | When the current event was delivered by email or webhook; `null` until it has been. |

```json
{
  "id": "hit_7fa176c8889c7569",
  "eventType": "new",
  "matchLabel": "dob_match",
  "record": {
    "name": "JORDAN RIVERA",
    "jurisdiction": "TX",
    "locality": "Houston",
    "matchLabel": "dob_match",
    "matchConfidence": 1.0,
    "personRef": "prs_3c9e1f0a7b2d4e6f8a1b",
    "recordId": "rec_ab12cd34ef56gh78ij90"
  },
  "firstSeenAt": "2026-09-06T12:00:41+00:00",
  "occurredAt": "2026-09-06T12:00:41+00:00",
  "notifiedAt": "2026-09-06T12:01:03+00:00"
}
```

## The AlertRecord object — the matched record in an alert

The matched record, summarised: `record` on an alert, `detail` on a webhook delivery. It names the person and a city, never a street address, and carries the `personRef` to recognise the person by. On a `removed` event it carries `event` and `jurisdiction`; reconcile a removal by the alert's `id`, which does not change. Run a search with the monitor's criteria, or fetch `recordId`, for the full record.

Returned or accepted by [`POST /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors), [`GET /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-get-v1-monitors), [`GET /v1/monitors/{monitorId}`](https://offendersearch.app/docs/monitoring/endpoints#reference-get-v1-monitors-id), [`GET /v1/monitors/{monitorId}/alerts`](https://offendersearch.app/docs/monitoring/alerts#alert-history), [`POST /v1/monitors/{monitorId}/webhook-secret`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors-id-webhook-secret).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` |  | The person's full name as the record publishes it. Absent on a `removed` event. Example `"JORDAN RIVERA"`. |
| `jurisdiction` | `string` |  | The two-letter code of the publishing jurisdiction (on a criminal record, its state or county code). Example `"TX"`. |
| `locality` | `string` |  | The record's residence city (or its first address city), never a street. `""` when the record publishes no city. Absent on a `removed` event. Example `"Houston"`. |
| `matchLabel` | `string` — one of `"dob_match"`, `"year_match"`, `"age_match"`, `"name_match"`, `"in_radius"` |  | How the record matched — the same value as the alert's `matchLabel`. Absent on a `removed` event. |
| `matchConfidence` | `number` or null |  | The record's match confidence from 0 to 1, or `null` when it was not scored. Absent on a `removed` event. Example `1`. |
| `personRef` | `string` or null |  | The person this alert is about, within the publishing registry or source — the same `personRef` the matching record carries in search results. It does not change when a registry republishes the listing under a new number, so de-duplicate and reconcile on it. `null` when the record carries none. Present on removed records too, so a removal can be matched to the person. Example `"prs_3c9e1f0a7b2d4e6f8a1b"`. |
| `recordId` | `string` or null |  | On a sex-offender monitor, the `recordId` of the matching record at the time of the event, for `GET /v1/records/{recordId}`. `null` on a criminal monitor. Example `"rec_ab12cd34ef56gh78ij90"`. |
| `externalId` | `string` or null |  | The record's own id at its source. Set for criminal records (the same `externalId` the Criminal Records API returns); `null` for registry records. |
| `recordType` | `string` or null |  | Criminal monitors: the matched record's `recordType` — `court_case`, `doc_inmate`, `jail_booking`, `healthcare_exclusion` and the other values the [criminal record](https://offendersearch.app/docs/criminal/record-object) carries. `null` when the record names none; absent on a sex-offender monitor and on a `removed` event. Example `"healthcare_exclusion"`. |
| `exclusion` | `object` |  | Present only when `recordType` is `healthcare_exclusion`: the exclusion list's own facts, so the alert can say "added to an exclusion list, authority 1128(a)(1)". On these records the event reads as added to (`new`), updated on (`changed`) or removed from (`removed`) an exclusion list. |
| `exclusion.authority` | `string` |  | The authority as the exclusion list labels it. Example `"1128(a)(1)"`. |
| `exclusion.convictionBased` | `boolean` |  | Whether the basis is a criminal conviction. Only conviction-based exclusions alert. |
| `exclusion.excludedOn` | `string` or null |  | ISO date the exclusion took effect. Example `"2021-03-19"`. |
| `event` | `string` — one of `"removed"` |  | Present only on a `removed` event, always `"removed"`. |

## The three event types

| `eventType` | Meaning |
| --- | --- |
| `new` | A person **newly matches** — someone who was not matching at the previous evaluation. On a location monitor, a match moved into the radius. The first evaluation after you create a monitor reports everyone who already matches as `new`. |
| `changed` | A fact about a person you are already matching **changed** — an address, an offense field, a status, or, on a criminal monitor, a new booking. A registry republishing the listing under a new number is not a change. |
| `removed` | A person who was matching **no longer matches** — they are no longer listed in that registry, or, on a location monitor, they moved out of the radius. Disappearance is data, so it is reported. |

## Cadence

Each monitor is evaluated **once per day**, against the continuously-updated dataset. An evaluation compares today’s matching records with the previous state and emits one alert per change. There is no faster tier and no way to force an off-cycle evaluation; a monitor is a daily watch by design.

## One alert per person

A monitor keys its alerts on the **person**, not the listing. Within one registry — or, on a criminal monitor, one source — the person is identified by `personRef`, the same value the matching record carries in search results, and that value stays the same when the registry republishes the listing under a new number. The consequences:

- **One thread per person per registry.** Someone listed in two registries is two alerts, one per registry; someone whose listing a registry renumbered is still one alert, with the same `id`.
- **A renumbered listing is not an event.** It produces no `new`, no `changed` and no `removed`. `new` means a person newly matches, `changed` means a fact about them changed, and `removed` means they are no longer listed in that registry.
- **The alert carries the identifiers.** `record.personRef` is what to reconcile your own files on, with `jurisdiction`. On a sex-offender monitor `record.recordId` is the matching record as it stood at the event, for `GET /v1/records/{recordId}`; read it as a pointer to that record, not as the person’s key.

## De-duplication

A monitor keeps **one alert per person per registry**. A person who stays matched and unchanged is silent after their first `new`. When a fact about a matched person changes, or they stop matching, the same alert — same `id` — takes the new `eventType` (`changed` or `removed`), its `notifiedAt` resets to `null`, and the event is delivered again.

> The alert history is the system of record. A webhook delivery carries the alert’s `id` as `alertId`, so de-duplicate deliveries on it and reconcile what your endpoint received against `GET /v1/monitors/{id}/alerts`, where every alert has a stable `id` and a `notifiedAt`.

## GET /v1/monitors/{id}/alerts — monitoring alert history API request and response fields

`GET /v1/monitors/{monitorId}/alerts` · auth: `X-API-Key`

One monitor's alerts, most recently observed first — the system of record for every alert the monitor has raised, whether or not a delivery reached you.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `monitorId` | `string` | required | The monitor `id` (`mon_…`). Another account's monitor id is a `404`. Example `"mon_b8f40d7afb9dc0d8"`. |

**Query parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `limit` | `integer` | optional · default `50` | How many alerts to return. Values below 1 are raised to 1 and values above 200 are lowered to 200. Example `100`. |

### Response

**200** (application/json) — The alerts.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `alerts` | array of [Alert](https://offendersearch.app/docs/monitoring/alerts#the-alert-object) |  | The monitor's alerts, newest `occurredAt` first. `[]` when the monitor has raised none. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` header (or dashboard session token). Body: [the Error object](https://offendersearch.app/docs/errors#object-error), `detail` a sentence. |
| 402 | — | The account has no active subscription and no active free trial. |
| 404 | — | No monitor with this id on your account: `{"detail": "Monitor not found."}`. |

```bash
curl "https://api.offendersearch.app/v1/monitors/mon_b8f40d7afb9dc0d8/alerts?limit=100" \
  -H "X-API-Key: $OFFENDERSEARCH_KEY"
```

> This history is not a consumer report and may not be used for any FCRA-covered decision. *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.).*

---

# Delivery — email & webhooks

> Alerts arrive by email, and by webhook when you configure one: the monitor.alert JSON payload, how to authenticate a delivery, and when an alert is sent again.

HTML: https://offendersearch.app/docs/monitoring/delivery · Markdown: https://offendersearch.app/docs/monitoring/delivery.md

## Two channels

Alerts are delivered to the `channels` you set on the monitor: email, a webhook, or both. A monitor needs at least one email address or a `webhookUrl` at creation. The account owner’s email receives the alert email in addition to the addresses you set.

| Channel | Set it with | What arrives |
| --- | --- | --- |
| Email | `channels.email`, `channels.emails` | One alert email per delivery run, listing every pending alert for that set of recipients. |
| Webhook | `channels.webhookUrl` | One JSON POST per alert — the `monitor.alert` payload below. |

## The Channels object — alert delivery request fields

Where a monitor's alerts are delivered: `channels` on `POST /v1/monitors`. The account owner's email always receives the alert email as well.

Returned or accepted by [`POST /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors).

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `email` | `string` or null | optional | One recipient address. Merged with `emails` into one de-duplicated list, returned on the monitor as `channels.emails`. Example `"alerts@example.com"`. |
| `emails` | `array of string` or null | optional | More recipient addresses, for a monitor whose alerts go to a team. Example `["hr@example.com","security@example.com"]`. |
| `webhookUrl` | `string (uri)` or null | optional | The URL that receives each alert as a JSON POST — the [monitor.alert webhook payload](https://offendersearch.app/docs/monitoring/delivery#object-webhookdelivery). Must be an `https://` URL on a public host name or public IP; anything else (a malformed URL, `http://`, `localhost`, a private, loopback or link-local address) is refused with `422`. Redirects are not followed. Example `"https://example.com/hooks/offendersearch"`. |

## Email

Every address in `channels.email` and `channels.emails`, plus the account owner, receives the alert email. Alerts for monitors with the same recipients are sent together, one email per run; each line gives the event, the monitor’s `label` (or the matched name), the city and jurisdiction, and the date. The email ends with the notice that alerts are not a consumer report.

## Signed webhooks

Set `channels.webhookUrl` and each alert is POSTed to that URL as JSON, with `Content-Type: application/json` and `User-Agent: Offendersearch-Monitor/1`. The body is the `monitor.alert` payload, defined below; its `detail` is the same record summary the alert history returns as `record`.

Every delivery is **signed with the monitor’s own secret**. The `201` that creates a monitor with a `webhookUrl` returns `channels.webhookSecret` (`whsec_…`) once — store it; it is never shown again. The header is `X-Offendersearch-Signature: t=<unix seconds>,v1=<hex>`, the HMAC-SHA256 with that secret of the string `<t>.<raw body>` — the same scheme the FCRA events use, so one verifier serves every OffenderSearch webhook. Verify before acting, compare in constant time, and refuse a timestamp outside your replay window. Rotate the secret at any time with `POST /v1/monitors/{monitorId}/webhook-secret`; the old one stops verifying at once.

```json
{
  "type": "monitor.alert",
  "event": "new",
  "alertId": "hit_7fa176c8889c7569",
  "monitorId": "mon_b8f40d7afb9dc0d8",
  "monitorLabel": "Jordan Rivera — SO watch",
  "product": "sex-offender",
  "detail": {
    "name": "JORDAN RIVERA",
    "jurisdiction": "TX",
    "locality": "Houston",
    "matchLabel": "dob_match",
    "matchConfidence": 1.0,
    "personRef": "prs_3c9e1f0a7b2d4e6f8a1b",
    "recordId": "rec_ab12cd34ef56gh78ij90"
  },
  "occurredAt": "2026-09-06T12:00:41+00:00",
  "sentAt": "2026-09-06T12:01:03+00:00"
}
```

```javascript
// channels.webhookSecret (whsec_…) was returned once, on the 201 that created the monitor.
import crypto from "node:crypto";
const SECRET = process.env.OS_WEBHOOK_SECRET;        // the monitor's whsec_… secret

app.post("/hooks/os", express.raw({ type: "application/json" }), (req, res) => {
  const header = String(req.get("X-Offendersearch-Signature") ?? "");   // "t=<unix>,v1=<hex>"
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", SECRET).update(`${parts.t}.`).update(req.body).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;   // five-minute replay window
  const ok = fresh && parts.v1?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);

  const alert = JSON.parse(req.body);           // type: "monitor.alert"
  enqueue(alert.alertId, alert);                // one alert per person per registry: de-duplicate on alertId
  res.sendStatus(200);                          // answer within 10 seconds
});
```

## The monitor.alert webhook payload object

The JSON body POSTed to `channels.webhookUrl`, one request per alert, with `Content-Type: application/json`, `User-Agent: Offendersearch-Monitor/1` and `X-Offendersearch-Signature: t=<unix seconds>,v1=<hex>` — the HMAC-SHA256, with the monitor's `webhookSecret`, of `<t>.<raw body>`. Verify it before acting (see Delivery).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `string` — one of `"monitor.alert"` |  | Always `monitor.alert`. |
| `event` | `string` — one of `"new"`, `"changed"`, `"removed"` |  | The event: `new`, `changed` or `removed`. Same meaning as the alert's `eventType`. |
| `alertId` | `string` |  | The `id` of the alert this delivery reports — `hit_` and 16 hex characters, the same value `GET /v1/monitors/{id}/alerts` lists. One alert per monitor and person per registry, so de-duplicate deliveries on it. Example `"hit_7fa176c8889c7569"`. |
| `monitorId` | `string` |  | The monitor that fired. One endpoint can serve every monitor on the account; route on this. Example `"mon_b8f40d7afb9dc0d8"`. |
| `monitorLabel` | `string` or null |  | The monitor's `label`, or `null` when it has none. |
| `product` | `string` — one of `"sex-offender"`, `"criminal"` |  | The monitor's dataset. |
| `detail` | [AlertRecord](https://offendersearch.app/docs/monitoring/alerts#object-alertrecord) |  | The matched record, summarised, with the same `personRef` and `recordId` the alert's `record` carries. |
| `occurredAt` | `string (date-time)` |  | When this record first matched the monitor (the alert's `firstSeenAt`). |
| `sentAt` | `string (date-time)` |  | When this delivery was sent. |

## Retries

Each pending alert is POSTed once per delivery run, with a 10-second timeout. An alert is marked delivered — its `notifiedAt` is set — as soon as either its email or its webhook POST succeeds (a `2xx` answer), and it is not sent again. When neither channel succeeds, the alert stays pending and both are tried again on the next run, the following day. There is no faster retry and no backoff schedule. Every alert is readable from `GET /v1/monitors/{id}/alerts` whatever happened to its delivery, so the history endpoint is the record to reconcile against.

> **Answer fast.** Acknowledge with a `2xx` inside 10 seconds, then do the work asynchronously. On a monitor with email and a webhook, a delivered email marks the alert delivered even when the webhook POST failed — read the alert history to catch anything your endpoint missed.

---

# Billing — one predicate, one charge

> The product and type matrix with the monthly price of each monitor, the rule that each dataset is a separate charge, and when a cancelled monitor stops being charged.

HTML: https://offendersearch.app/docs/monitoring/billing · Markdown: https://offendersearch.app/docs/monitoring/billing.md

## The price matrix

A monitor is a flat monthly charge. Price depends on the `type` — a person watch or a location watch — and every monitor watches exactly one `product`.

| Product | Type | Status | Price |
| --- | --- | --- | --- |
| Sex-offender | person | **Live** | $3 / month |
| Criminal | person | **Live** | $3 / month |
| Sex-offender | location | **Live** | $10 / month |
| Criminal | location | **Coming soon** | $10 / month |

> **Criminal location monitoring is coming soon** — it will arrive as location coverage across criminal records expands. A `POST` for it returns a `not_yet_available` acknowledgement and bills nothing (see [Location monitoring](https://offendersearch.app/docs/monitoring/location-monitoring.md)).

## One product, one predicate, one charge

This is the rule to build your billing model on: **each monitor is one product, one predicate, and one charge.** Criminal and sex-offender person-monitoring are **two separate monitors**, even for the same person. Watching one individual across **both** datasets means two monitors — one `product: "sex-offender"` and one `product: "criminal"` — and therefore **two charges**: $3 + $3 = **$6 / month**.

```json
// Watching Jordan Rivera across BOTH datasets = two monitors = $6/mo.

// Monitor 1 — $3/mo
{ "product": "sex-offender", "type": "person",
  "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" },
  "channels": { "email": "alerts@example.com" } }

// Monitor 2 — $3/mo  (a separate POST, a separate mon_… id, a separate charge)
{ "product": "criminal", "type": "person",
  "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" },
  "channels": { "email": "alerts@example.com" } }
```

There is no combined "watch everywhere" monitor. If you want one person covered across both datasets, create two monitors and expect two charges.

## Invoicing

Monitor charges appear on the **same monthly invoice** as the rest of your account’s usage — the per-call search and live-verification charges from the other products sit alongside your monitors. Every monitor that is still `active` when the month’s invoice is prepared is charged its full price from the matrix above, and the monitors are totalled on one Monitoring line.

## Cancelling and proration

Cancel a monitor with `DELETE /v1/monitors/{id}`. Cancelling sets its `status` to `canceled`, and the monitor stops being evaluated at once. It is **not charged on any invoice prepared after you cancel it**, including the invoice for the month you cancel in. Charges are not prorated: a monitor that is active when an invoice is prepared is charged the full monthly price. A cancelled monitor stays readable, so its alert history is preserved.

```bash
curl -X DELETE "https://api.offendersearch.app/v1/monitors/mon_b8f40d7afb9dc0d8" \
  -H "X-API-Key: $OFFENDERSEARCH_KEY"
```

---

# Endpoint & error reference

> The five monitoring endpoints — create, list, retrieve, cancel and alert history — with request and response bodies, and the error envelope you switch on.

HTML: https://offendersearch.app/docs/monitoring/endpoints · Markdown: https://offendersearch.app/docs/monitoring/endpoints.md

## The five endpoints

Every request authenticates with the `X-API-Key` header. Base URL `https://api.offendersearch.app`. Each endpoint below has a Request table and a Response table listing every field; the Monitor and alert objects are linked to their own pages, not repeated.

| Method | Path | What it does |
| --- | --- | --- |
| `POST` | `/v1/monitors` | Create a monitor. |
| `GET` | `/v1/monitors` | List monitors. |
| `GET` | `/v1/monitors/{monitorId}` | Retrieve a monitor. |
| `DELETE` | `/v1/monitors/{monitorId}` | Cancel a monitor. |
| `GET` | `/v1/monitors/{monitorId}/alerts` | Alert history. |
| `POST` | `/v1/monitors/{monitorId}/webhook-secret` | Rotate the webhook secret. |

## POST /v1/monitors — create monitor request and response fields

`POST /v1/monitors` · auth: `X-API-Key`

Create a person or location monitor on the sex offender or criminal dataset. Returns the new monitor with `201`.

### Request

**Body** (application/json)

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `product` | `string` — one of `"sex-offender"`, `"criminal"` | required | The dataset this monitor watches. One product per monitor: watching one person in both datasets is two monitors, billed separately. Example `"sex-offender"`. |
| `type` | `string` — one of `"person"`, `"location"` | required | The predicate. `person` watches a name (and date of birth); `location` watches a radius around a point. `criminal` with `location` returns `200` `not_yet_available` and creates nothing. Example `"person"`. |
| `person` | [PersonCriteria](https://offendersearch.app/docs/monitoring/person-monitoring#object-personcriteria) or null | optional | Required when `type` is `person`; ignored otherwise. |
| `location` | [LocationCriteria](https://offendersearch.app/docs/monitoring/location-monitoring#object-locationcriteria) or null | optional | Required when `type` is `location`; ignored otherwise. |
| `minConfidence` | `string` or null — one of `"dob_match"`, `"year_match"`, `"age_match"`, `"name_match"`, `"any"` | optional | The weakest match strength a person monitor alerts on, strongest first: `dob_match`, `year_match`, `age_match`, `name_match`. Unset or `any` applies the default floor: `year_match` when `person.dob` is set, `name_match` when it is not. Location monitors alert on every record in the radius and do not read this field. Any other value is a `422`. Example `"dob_match"`. |
| `label` | `string` or null | optional | Your own name for the monitor. Echoed back on the monitor and in the alert email and webhook (`monitorLabel`). Example `"Jordan Rivera — SO watch"`. |
| `channels` | [Channels](https://offendersearch.app/docs/monitoring/delivery#object-channels) | optional | Where alerts go. Send at least one email address or a `webhookUrl`, or the request is a `422`. |

### Response

**201** (application/json) — The monitor was created and is `active`.

The body is [the `Monitor` object](https://offendersearch.app/docs/monitoring/monitor-object#fields). One standing watch, as `POST /v1/monitors`, `GET /v1/monitors` and `GET /v1/monitors/{monitorId}` return it. A person monitor carries `person` and no `location` key; a location monitor carries `location` and no `person` key — read `type`, then the matching object.

**200** (application/json) — `product: "criminal"` with `type: "location"`. No monitor was created and nothing is billed; branch on `status`.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `status` | `string` — one of `"not_yet_available"` |  | Always `not_yet_available`. |
| `message` | `string` |  | A sentence you can show a user. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` header (or dashboard session token). Body: [the Error object](https://offendersearch.app/docs/errors#object-error), `detail` a sentence. |
| 402 | — | The account has no active subscription and no active free trial. |
| 422 | — | Validation failed. `detail` is a sentence naming the field for an unknown `product`, `type` or `minConfidence`, no channel, a person monitor without `person.lastName`, a `person.dob` that is not `YYYY-MM-DD`, a location monitor without `location.address` or `lat`+`lng`, or `radiusMiles` outside (0, 100]. A missing `product` or `type`, or a wrongly typed field, returns `detail` as a list of `{type, loc, msg}` objects. |

```bash
curl -X POST "https://api.offendersearch.app/v1/monitors" \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" -H "Content-Type: application/json" \
  -d '{ "product": "sex-offender", "type": "person",
        "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" },
        "channels": { "email": "alerts@example.com" } }'
```

> A `POST` for `product: "criminal"` with `type: "location"` returns a `200` with `status: "not_yet_available"` — no monitor is created and nothing is billed. See [Location monitoring](https://offendersearch.app/docs/monitoring/location-monitoring.md).

## GET /v1/monitors — list monitors request and response fields

`GET /v1/monitors` · auth: `X-API-Key`

Every monitor on the account, newest first. Active monitors only unless you pass `includeCanceled=true`.

### Request

**Query parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `includeCanceled` | `boolean` | optional · default `false` | `true` also returns canceled monitors. |

### Response

**200** (application/json) — The monitors.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `monitors` | array of [Monitor](https://offendersearch.app/docs/monitoring/monitor-object#fields) |  | The account's monitors, newest `createdAt` first. `[]` when there are none. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` header (or dashboard session token). Body: [the Error object](https://offendersearch.app/docs/errors#object-error), `detail` a sentence. |
| 402 | — | The account has no active subscription and no active free trial. |

```json
{
  "monitors": [
    { "id": "mon_b8f40d7afb9dc0d8", "product": "sex-offender", "type": "person",
      "minConfidence": null, "label": "Jordan Rivera — SO watch", "status": "active",
      "channels": { "emails": ["alerts@example.com"] },
      "createdAt": "2026-09-05T14:02:11+00:00", "canceledAt": null,
      "person": { "firstName": "Jordan", "lastName": "Rivera", "dob": "1988-04-12" } }
  ]
}
```

## GET /v1/monitors/{id} and /alerts

`GET /v1/monitors/{id}` returns one monitor with its ten most recently observed alerts in `recentAlerts`; `GET /v1/monitors/{id}/alerts` returns its alert history, up to 200 alerts per call (`limit`, default 50). Both are scoped to the creating account; another account’s `id` returns `404`. The fields are defined on [The Monitor object](https://offendersearch.app/docs/monitoring/monitor-object.md) and [Alerts](https://offendersearch.app/docs/monitoring/alerts.md).

## GET /v1/monitors/{id} — retrieve monitor request and response fields

`GET /v1/monitors/{monitorId}` · auth: `X-API-Key`

One monitor with `recentAlerts`, its ten most recently observed alerts. Works on `active` and `canceled` monitors.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `monitorId` | `string` | required | The monitor `id` (`mon_…`). Another account's monitor id is a `404`. Example `"mon_b8f40d7afb9dc0d8"`. |

### Response

**200** (application/json) — The monitor, including `recentAlerts`.

The body is [the `Monitor` object](https://offendersearch.app/docs/monitoring/monitor-object#fields). One standing watch, as `POST /v1/monitors`, `GET /v1/monitors` and `GET /v1/monitors/{monitorId}` return it. A person monitor carries `person` and no `location` key; a location monitor carries `location` and no `person` key — read `type`, then the matching object.

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` header (or dashboard session token). Body: [the Error object](https://offendersearch.app/docs/errors#object-error), `detail` a sentence. |
| 402 | — | The account has no active subscription and no active free trial. |
| 404 | — | No monitor with this id on your account: `{"detail": "Monitor not found."}`. |

## DELETE /v1/monitors/{id} — cancel monitor request and response fields

`DELETE /v1/monitors/{monitorId}` · auth: `X-API-Key`

Cancel a monitor. It stops being evaluated at once and is not charged on any invoice prepared after the cancel. The monitor and its alerts stay readable with `GET /v1/monitors/{monitorId}`.

### Request

**Path parameters**

| Field | Type | Required / default | Description |
| --- | --- | --- | --- |
| `monitorId` | `string` | required | The monitor `id` (`mon_…`). Another account's monitor id is a `404`. Example `"mon_b8f40d7afb9dc0d8"`. |

### Response

**200** (application/json) — The monitor is now `canceled`.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `status` | `string` — one of `"canceled"` |  | Always `canceled`. |
| `id` | `string` |  | The canceled monitor's id. |
| `note` | `string` |  | A fixed sentence about when billing ends. Evaluation stops as soon as the monitor is canceled. |

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 401 | — | No valid `X-API-Key` header (or dashboard session token). Body: [the Error object](https://offendersearch.app/docs/errors#object-error), `detail` a sentence. |
| 402 | — | The account has no active subscription and no active free trial. |
| 404 | — | No monitor with this id on your account: `{"detail": "Monitor not found."}`. |
| 409 | `personal_alert` | The monitor is a personal alert paid by its own subscription; it is canceled from Alerts in the personal dashboard. `detail` is `{code, message}`. |

A canceled monitor stops being evaluated at once and stays readable, so its alert history is preserved.

```json
{
  "status": "canceled",
  "id": "mon_b8f40d7afb9dc0d8",
  "note": "Monitoring stops and billing ends at the close of the current cycle."
}
```

## The error envelope

Every monitoring error is a JSON object with one key, `detail`. For most errors `detail` is a sentence you can show a user. A body that fails type validation (a missing `product`, a string where a number belongs) returns `detail` as a list of `{ type, loc, msg }` objects, where `loc` is the path to the field. Branch on the HTTP status; read `detail` for the reason. The shape is [the Error object](https://offendersearch.app/docs/errors#object-error) shared by every OffenderSearch API.

```json
{ "detail": "channels must include at least one email or a webhookUrl." }
```

| HTTP | When you will see it |
| --- | --- |
| `401` | No valid `X-API-Key` header (or dashboard session token). |
| `402` | The account has no active subscription and no active free trial. |
| `404` | No monitor with that id on your account — including another account’s monitor id. |
| `409` | DELETE on a personal alert, which is billed by its own subscription. `detail` is `{ code: "personal_alert", message }`. |
| `422` | The create body failed validation. `detail` is a sentence naming the field, or a list of `{ type, loc, msg }` objects for a missing or wrongly typed field. |

> **The monitoring endpoints do not return `429`.** Apply your own client-side concurrency control.

> Monitoring alerts are not a consumer report, and monitoring is not part of FCRA Partner Access. *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.).* Every alert email carries a not-a-consumer-report notice in its footer.

---

# Monitoring API reference

> Every Monitoring API endpoint and object in one index — create, list, retrieve, cancel, alert history, the Monitor, the alert and the webhook payload.

HTML: https://offendersearch.app/docs/monitoring/api-reference · Markdown: https://offendersearch.app/docs/monitoring/api-reference.md

## Sex offender monitoring API endpoints

Every monitoring endpoint, each linked to its Request and Response tables. All five authenticate with the `X-API-Key` header and live under `https://api.offendersearch.app`.

| Endpoint | What it does |
| --- | --- |
| [`POST /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors) | **Create a monitor.** Create a person or location monitor on the sex offender or criminal dataset. Returns the new monitor with `201`. |
| [`GET /v1/monitors`](https://offendersearch.app/docs/monitoring/endpoints#reference-get-v1-monitors) | **List monitors.** Every monitor on the account, newest first. Active monitors only unless you pass `includeCanceled=true`. |
| [`GET /v1/monitors/{monitorId}`](https://offendersearch.app/docs/monitoring/endpoints#reference-get-v1-monitors-id) | **Retrieve a monitor.** One monitor with `recentAlerts`, its ten most recently observed alerts. Works on `active` and `canceled` monitors. |
| [`DELETE /v1/monitors/{monitorId}`](https://offendersearch.app/docs/monitoring/endpoints#reference-delete-v1-monitors-id) | **Cancel a monitor.** Cancel a monitor. It stops being evaluated at once and is not charged on any invoice prepared after the cancel. The monitor and its alerts stay readable with `GET /v1/monitors/{monitorId}`. |
| [`GET /v1/monitors/{monitorId}/alerts`](https://offendersearch.app/docs/monitoring/alerts#alert-history) | **Alert history.** One monitor's alerts, most recently observed first — the system of record for every alert the monitor has raised, whether or not a delivery reached you. |
| [`POST /v1/monitors/{monitorId}/webhook-secret`](https://offendersearch.app/docs/monitoring/endpoints#reference-post-v1-monitors-id-webhook-secret) | **Rotate the webhook secret.** Issue a new signing secret for the monitor's webhook. The previous secret stops verifying at once. The new `whsec_…` value is returned in this response only. |

## Monitoring API objects

Every object the monitoring endpoints accept or return, and the webhook payload they deliver. Each one is defined once, on the page linked here, and every other mention links back to it.

| Object | What it is |
| --- | --- |
| [`PersonCriteria`](https://offendersearch.app/docs/monitoring/person-monitoring#object-personcriteria) | Who a person monitor watches: `person` on `POST /v1/monitors`. Returned on the monitor as `person`. |
| [`LocationCriteria`](https://offendersearch.app/docs/monitoring/location-monitoring#object-locationcriteria) | The centre and radius a location monitor watches: `location` on `POST /v1/monitors`. Returned on the monitor as `location`. |
| [`Channels`](https://offendersearch.app/docs/monitoring/delivery#object-channels) | Where a monitor's alerts are delivered: `channels` on `POST /v1/monitors`. The account owner's email always receives the alert email as well. |
| [`Monitor`](https://offendersearch.app/docs/monitoring/monitor-object#fields) | One standing watch, as `POST /v1/monitors`, `GET /v1/monitors` and `GET /v1/monitors/{monitorId}` return it. A person monitor carries `person` and no `location` key; a location monitor carries `location` and no `person` key — read `type`, then the matching object. |
| [`Alert`](https://offendersearch.app/docs/monitoring/alerts#the-alert-object) | One person's latest event on one monitor, as the alert history and `recentAlerts` return it. There is one alert per monitor and person per registry: when a matched person's record changes again, or a registry republishes it under a new number, the alert keeps the same `id` and (on a change) takes the new `eventType`. |
| [`AlertRecord`](https://offendersearch.app/docs/monitoring/alerts#object-alertrecord) | The matched record, summarised: `record` on an alert, `detail` on a webhook delivery. It names the person and a city, never a street address, and carries the `personRef` to recognise the person by. On a `removed` event it carries `event` and `jurisdiction`; reconcile a removal by the alert's `id`, which does not change. Run a search with the monitor's criteria, or fetch `recordId`, for the full record. |
| [`WebhookDelivery`](https://offendersearch.app/docs/monitoring/delivery#object-webhookdelivery) | The JSON body POSTed to `channels.webhookUrl`, one request per alert, with `Content-Type: application/json`, `User-Agent: Offendersearch-Monitor/1` and `X-Offendersearch-Signature: t=<unix seconds>,v1=<hex>` — the HMAC-SHA256, with the monitor's `webhookSecret`, of `<t>.<raw body>`. Verify it before acting (see Delivery). |
