Continuous criminal monitoring API
Create a standing watch on a person with one POST and receive a signed monitor.alert when a court case, booking, custody record or warrant appears, changes or is removed. $3.00 a person a month; the history endpoint is the record when a webhook is missed.
Updated November 8, 2026 · API changelog
curl -X POST https://api.offendersearch.app/v1/monitors \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"product": "criminal",
"type": "person",
"person": {
"firstName": "Felony",
"lastName": "Record",
"dob": "1975-06-15"
},
"channels": {
"email": "alerts@your-app.example.com"
}
}'
# 201 Created; with a channels.webhookUrl the body also carries channels.webhookSecret, once{
"id": "mon_5f67d9bde1f79980",
"product": "criminal",
"type": "person",
"minConfidence": null,
"label": null,
"status": "active",
"channels": {
"emails": [
"alerts@your-app.example.com"
]
},
"createdAt": "2026-11-08T15:49:29.914226+00:00",
"canceledAt": null,
"person": {
"firstName": "Felony",
"lastName": "Record",
"dob": "1975-06-15"
},
"sandbox": true
}new, changed, removedCreate a continuous criminal monitor with one POST
One product, one predicate, at least one channel. The body above, field by field.
| Field | Values | What it decides |
|---|---|---|
product | criminal or sex-offender | Which dataset the monitor watches. A worker often gets one of each. |
type | person (location for registries) | A named person, or a radius around a point. |
person | lastName required; firstName, dob | The name and date of birth you searched at onboarding; the date of birth decides how strong a match can be. |
minConfidence | dob_match · year_match · age_match · name_match · any | The weakest match that fires an alert. Unset, it is year_match with a dob and name_match without one. |
channels | webhookUrl, email, emails[] | Where alerts go. A webhookUrl returns channels.webhookSecret on the 201, once. |
label | free text | Echoed on every alert, so a receiver can route without a lookup. |
The 201 is the monitor object: an id (mon_ + 16 hex) that keys every later call, status: active, the channels as stored and the person as you sent them. minConfidence comes back null when you left it unset, which means the default floor, not “no floor”.
Reading it back is GET /v1/monitors/{id}; listing them is GET /v1/monitors; stopping one is DELETE /v1/monitors/{id}, which ends evaluation and billing but keeps the alert history readable. The monitor object reference has each field’s type.
A platform with a roster creates one monitor per active worker, stores id and webhookSecret beside the worker, and lets the workforce screening API flow handle the onboarding search and the scheduled re-screen around it.
The monitor.alert webhook: payload, header and signature
What arrives at your webhookUrl, captured from the sandbox’s test-event endpoint. The record summary is the same shape the alert history returns.
{
"alert": {
"id": "hit_0062be162aca3cb4",
"eventType": "new",
"matchLabel": "dob_match",
"record": {
"name": "Felony Record",
"jurisdiction": "OH",
"locality": "Sampleville",
"matchLabel": "dob_match",
"matchConfidence": null,
"recordType": "court_case",
"personRef": "prs_6e1aaefd188032a7e6eb",
"externalId": "SBX-CR-2015-000123"
},
"firstSeenAt": "2026-11-08T15:49:29.915412+00:00",
"occurredAt": "2026-11-08T15:49:29.915412+00:00",
"notifiedAt": null
},
"webhookDelivered": false,
"sandbox": true
}// X-Offendersearch-Signature: t=<unix seconds>,v1=<hex>
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) reject(); // five-minute replay window
const expected = crypto.createHmac('sha256', secretFor(body.monitorId)) // one secret per monitor
.update(`${parts.t}.`).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) reject();| Header | Value |
|---|---|
X-Offendersearch-Signature | t=<unix seconds>,v1=<hex>; v1 = HMAC-SHA256(secret, <t>.<raw body>) |
Content-Type | application/json |
User-Agent | Offendersearch-Monitor/1 |
| Replay window | Refuse a timestamp more than five minutes from your clock, measured against the header, not the body |
| Secret | Per monitor, whsec_…, shown once on the 201; rotate with POST /v1/monitors/{id}/webhook-secret |
| Field | Type | What it tells you |
|---|---|---|
| alert.id | string | hit_ + hex. One alert per person per source; the same id is delivered again with a new eventType when the listing changes or is removed. |
| alert.eventType | enum | new, changed or removed: what happened to the person since the previous evaluation. |
| alert.matchLabel | enum | dob_match, year_match, age_match, name_match; null on a removed event. |
| alert.record | object | A summary of the record: name, jurisdiction, locality, recordType, externalId and personRef. The full record is one search away. |
| alert.record.personRef | string | The stable handle for the person. Key your own rows on this and on the alert id, never on a listing number. |
| alert.firstSeenAt, occurredAt | datetime | When the person first matched this monitor, and when this event was filed. |
| alert.notifiedAt | datetime | null | null until a delivery succeeded; the history endpoint shows it, so a missed webhook is visible. |
| webhookDelivered | boolean | Sandbox only: whether the test event reached your webhookUrl. Production reports delivery through notifiedAt. |
The scheme is the same one FCRA events use, so one verifier serves every webhook this platform sends. The webhook example has the Express and FastAPI receivers in full, the API signature page has a known-answer test vector, and the webhook tester signs a sample monitor.alert with a secret you choose so the receiver can be exercised before any monitor exists.
Event catalogue for a criminal monitor: new, changed, removed
Three values, and a rule about identity that decides how you store them.
| eventType | When it fires on a criminal monitor | What the alert carries | What to do |
|---|---|---|---|
new | A court case, booking, custody record, warrant or conviction-based exclusion matches the person for the first time. On the first evaluation everyone who already matches is new. | The full summary: match label, record type, jurisdiction, externalId, personRef | Open a review; fetch the record with a search on the personRef’s name and date of birth |
changed | A fact about an already-matched person changed: a disposition, a status, a new booking on the same person. A jurisdiction renumbering its listing is not a change. | The same alert id with the new label and summary | Re-read the record; a disposition arriving on an open case is the common case |
removed | The person is no longer listed by that source. Reported once. | A sparse record: event, jurisdiction, personRef; matchLabel is null | Route to a person: disappearance is data, and the alert deliberately says little |
A monitor keeps one alert per person per source. When a fact changes or the person stops matching, the same alert, with the same id, takes the new eventType, has its notifiedAt reset and is delivered again. Nothing is keyed on a listing number, which is why a jurisdiction republishing a record under a new number produces no event at all.
Match labels are the same five the search uses, with in_radius added for a location monitor: dob_match, year_match, age_match, name_match, and null on a removal. A receiver that acts automatically only on dob_match and routes the rest to a person follows the same rule the matching reference gives for a search.
Store alerts as rows keyed on the alert id, set each row’s state from eventType, and record notifiedAt to know whether a delivery reached you. Deduplicate deliveries on the pair (alertId, eventType), not on the id alone: the same id legitimately arrives again as the listing moves from new to changed to removed.
The recentAlerts array on a monitor object is a ten-alert preview; the full history, newest first, is GET /v1/monitors/{id}/alerts. The monitor a person with webhooks tutorial builds the receiver and the reconciliation job end to end.
Delivery, retries and reconciling a missed criminal monitoring alert
At-least-once delivery, a short timeout, and a history endpoint that is the record whatever happened on the wire.
Each pending alert is POSTed once per evaluation with a 10-second timeout. An alert counts as delivered, and its notifiedAt is set, as soon as either its email or its webhook POST gets a 2xx; it is then not sent again. When neither channel succeeds the alert stays pending and both are tried on the next evaluation. There is no faster retry and no backoff schedule to tune.
That design puts the burden where it belongs: the history endpoint. Because an alert is written before it is delivered, a reconciliation job that walks GET /v1/monitors, reads each monitor’s alerts and processes any (id, eventType) pair it has not seen closes every gap without a resend endpoint. Run it after a deploy, after an outage, or on a schedule you choose.
| Situation | What the platform does | What your receiver sees |
|---|---|---|
| Endpoint answers 200 in time | Sets notifiedAt; no further delivery for this event | One POST |
| Endpoint times out or answers 5xx | Leaves the alert pending; tries again on the next evaluation | The same (alertId, eventType) again later |
| 200 lost on the network | Cannot tell it from a failure; redelivers | A duplicate, which the pair-based dedupe discards |
| Endpoint down for days | Alerts accumulate with notifiedAt: null | Everything, on the first successful evaluation after recovery, or sooner from GET …/alerts |
| Secret rotated | Old secret stops verifying at once; new one in the 200 only | Deliveries signed with the new secret from the next evaluation |
The delivery reference states the schedule and the exact retry rule; rate limits, retries and idempotency covers the receiving side’s own backoff when it calls the API back.
Continuous criminal monitoring versus a scheduled re-screen
Both keep a roster current after onboarding. Choosing between them is a question of latency and cost per person.
| Continuous monitor | Scheduled re-screen | |
|---|---|---|
| Latency to a new record | The next evaluation after the source lists it | Your next batch run |
| Cost per person | $3.00 a month | $0.15 a row per run, graduated past 2,000 in a month |
| What you receive | A signed monitor.alert with the delta | The full result for each row, and the diff is yours to compute |
| Identity across runs | personRef on the alert | results[].index → personRef per row |
| Best for | Active workers, residents, licensees | Whole rosters, including inactive people |
| Endpoint | POST /v1/monitors | POST /v1/criminal/batch |
Many teams run both: a monitor on each active person and a re-screen of the whole roster on a schedule of their own. The bulk criminal record search page covers the batch side; the criminal records API page documents the record a monitor summarises.
Continuous criminal monitoring API pricing
Per active monitor per month, alerts included, no per-alert fee. A search to read the full record is a call.
| Item | Price | Unit |
|---|---|---|
| Person monitor | $3.00 | per person a month |
| Location monitor | $10.00 | per location a month |
| Criminal or registry search, to read a record in full | $0.15 | per call, the first 2,000 in a calendar month |
| Free to start | 25 calls | once per account, on production data, no card |
| Sandbox | $0 | 60 calls a minute and 2,000 a day per test key |
No minimum on search. The Map API has its own $300 monthly minimum. No seats, no platform fee, no per-state fee; a batch row is billed as one call.
Sandbox monitors and test events are never billed. The monitoring billing reference states how a month is counted.
Worked examples by volume: background check API pricing · every rate: pricing.
Continuous monitoring compared with Checkr’s published price
Checkr lists continuous criminal search as an add-on at $1.70 per individual a month on its pricing page; the footnote gives the date it was checked.
| Aspect | Checkr | OffenderSearch API |
|---|---|---|
| Continuous monitoring | Checkr offers continuous criminal search at $1.70 per individual a month and continuous MVR at $2.50 per driver a month | $3.00 a month per person, $10.00 per location, alerts by signed webhook |
| Webhooks | webhooks signed with HMAC-SHA256 in an X-Checkr-Signature header; retries every minute for 10 minutes, then every hour for 24 hours | Async searches post the finished envelope to a webhookUrl; monitor and FCRA deliveries carry X-Offendersearch-Signature (HMAC-SHA256 over t.body), are retried and can be replayed |
| What you pay for | A finished consumer report, per report, by package; court and database fees passed through at cost | One API call: scored public records for a name and date of birth, in the same HTTP response |
| Test environment | a staging environment with mocked data, available once Checkr grants access (api.checkr-staging.com) | Self-serve sandbox with documented test people; mint an os_test_ key in the dashboard |
Checkr: checkr.com/pricing and docs.checkr.com, read 2026-11-08. Each vendor’s pages are the authority on its product and change without notice; a blank is a fact the vendor does not publish, not a zero. A consumer report and a public-records lookup are different products.
A monitor alert is not a consumer report
The alert tells you there is something to look at. A decision about a person runs on the second path, where FCRA partners can also enrol a subject in a continuous check that places an order when a candidate record appears.
Data API, not FCRA
- For: visitor and volunteer screening, trust and safety, research, monitoring, and software that shows a public record to the person who asked for it.
- Must not decide: whether someone is hired, housed, insured or given credit. The response is a public-records lookup, not a consumer report, and the key is not approved for those uses.
- Example: An admissions desk screens a visitor against the registries before a visit; no hiring, housing or credit decision is involved.
Self-serve: get a key, then POST /v1/search or /v1/criminal/search.
FCRA Partner Access
- Built in: end-user registration with a permissible purpose, consent on every order, the pre-adverse and adverse-action notices with the consumer’s copy, and a consumer portal.
- Disputes: a dispute endpoint; the record is re-checked with the registry or court that holds it, with an outcome inside 30 days, and a correction reaches every partner that received it.
- Example: A screening company furnishes a report an employer or landlord will act on; the order runs the consent, notice and dispute steps.
Under a written agreement: Request FCRA Partner Access.
The records can be the same; the process around them is not. How the program works: FCRA Partner Access · the orders endpoint: FCRA orders API · reference and statute citations: FCRA docs. This page describes the product, not legal advice.
Limits of the continuous criminal monitoring API
Rate limits, the response cap on a follow-up search, and what a monitor does not do.
| Limit | Value |
|---|---|
| Sandbox, per test key | 60 calls a minute, 2,000 a day; nothing billed |
| Production, per key | No published ceiling. A 429 only when the service is at capacity, with Retry-After; a 429 is never billed |
| Records per response | Up to 4,000 in one answer; above that capped: true, and perPage pages through the rest |
| Batch | 1,000 rows a call; a failing row is reported in place and never stops the batch |
| Deadline | deadlineMs bounds the wait for slow jurisdictions; an unanswered one is named in sourceStatus and the answer says partial |
| Endpoints | POST /v1/criminal/search; one X-API-Key header |
- No civil cases, no traffic citations. Criminal records only: jail bookings, prison custody, court cases with dispositions, warrants, and the registries.
- No international records. US county, state and federal sources, and the 58 US registries.
- Not a consumer report. A hiring, housing, credit or insurance decision runs as an order through FCRA Partner Access, not through the standard key.
- No SSN, no credit, no verifications, no drug or driving records. Scope comes from the name, the date of birth and the places you already hold.
- No criminal location monitor yet.
type: "location"withproduct: "criminal"answers200 not_yet_available; location monitors watch registered addresses on the registries. - No live re-verification inside a monitor. An alert summarises the stored record; confirm it with the jurisdiction through a
liveblock on a search. - Monitor endpoints:
POST,GET,DELETE /v1/monitors[/{id}],GET /v1/monitors/{id}/alerts,POST /v1/monitors/{id}/webhook-secret; sandbox only,POST /v1/sandbox/monitors/{id}/test-event.
Retry rules and backoff: rate limits, retries and idempotency · every status code: errors.
HTTP/1.1 429 Too Many Requests Retry-After: 12 Content-Type: application/json { "detail": { "code": "overloaded", "retryAfterSeconds": 12 } }Nothing was searched and nothing was billed; wait
retryAfterSeconds and send the same request again.Continuous criminal monitoring API questions
Evaluation, signing, outages, duplicates, scale and the FCRA line.
How is a monitor evaluated, and when does the first alert arrive?
In production the monitor is evaluated on the platform’s schedule and reports everyone who already matches as new on the first evaluation. On the sandbox it is evaluated once, at creation, so a monitor on the felony persona files its first alert within moments of the 201. The delivery reference states the schedule.
What is signed, and with which secret?
The raw request body, with the header’s own timestamp: v1 is HMAC-SHA256 over <t>.<raw body>. The secret is the monitor’s own whsec_ value, returned once on the 201, so a receiver reads monitorId from the body to pick the secret and then verifies. Rotate it with POST /v1/monitors/{id}/webhook-secret; the old one stops verifying at once.
What if my endpoint was down when an alert fired?
Delivery is at-least-once. A delivery that did not get a 2xx within 10 seconds leaves the alert pending with notifiedAt: null, and it is tried again on the next evaluation. The alert is already readable at GET /v1/monitors/{id}/alerts, so a reconciliation job can process it without waiting for the retry.
Why do I get the same alert id twice?
Because the alert is the person, not the event. When a matched person’s listing changes, the same alert takes eventType: changed and is delivered again; when they stop being listed it takes removed. Deduplicate on the pair (alertId, eventType), and treat that pair as the unit of work.
Can one webhook endpoint serve thousands of monitors?
Yes. Each payload carries monitorId so one route can look up the right secret, and User-Agent: Offendersearch-Monitor/1 identifies the sender. Keep the key and the secrets server-side; the endpoint needs HTTPS and a fast 200.
Is a location monitor available for criminal records?
Not yet. A location monitor ($10.00 a month) watches a radius against the registered addresses the sex offender registries publish; a criminal location monitor answers 200 not_yet_available. Criminal monitoring is per person.
Does a monitor alert let me take action against a worker?
Not by itself. Monitoring through the standard key is a public-records lookup, not a consumer report. A decision about employment, housing or credit that follows an alert belongs on the second path, FCRA Partner Access ($4.95 an order); FCRA partners can also enrol a subject in a continuous check that places an order automatically when a new candidate record appears.
How do I stop watching someone?
DELETE /v1/monitors/{id} stops evaluation and billing for that monitor; its alert history stays readable. Billing is per active monitor per month, so a monitor cancelled mid-month is not billed again.
Learn the API by example
Runnable guides that use this endpoint as the worked example.
All guides: Learn APIs by example · reference: developer docs · packages: SDKs and CLI.
Related background check API pages
The rest of the developer surface, one anchor per page.
Create a monitor on the sandbox and catch its first alert
Point channels.webhookUrl at the webhook tester, POST the body above with an os_test_ key, and verify the signature in your browser.
The OffenderSearch APIs
One account, one key, one bill.