# 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
- **Base URL (sandbox):** https://sandbox.offendersearch.app — test keys (`os_test_…`), never billed; see https://offendersearch.app/docs/monitoring/sandbox.md
- **Base URL (live):** https://api.offendersearch.app — live keys (`os_live_…`)
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Monitoring reference as markdown:** https://offendersearch.app/docs/monitoring.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).

---

## Related

- Previous: [Access, keys & data handling](https://offendersearch.app/docs/monitoring/authentication.md)
- Next: [The Monitor object](https://offendersearch.app/docs/monitoring/monitor-object.md)
- Index: [Monitoring API reference](https://offendersearch.app/docs/monitoring.md)
