OffenderSearch
Continuous criminal monitoring API · person monitors, signed webhooks

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.

Get a free API keyRead the docs25 free calls · no card

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
201 Created · captured from the sandbox
{
  "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
}
$3.00a person a month, alerts included
$10.00a location a month, registries only
1,934live criminal jurisdictions a monitor watches, as of 2026-11-08
58US sex offender registries, for a registry monitor on the same endpoint
3events: new, changed, removed

Create a continuous criminal monitor with one POST

One product, one predicate, at least one channel. The body above, field by field.

FieldValuesWhat it decides
productcriminal or sex-offenderWhich dataset the monitor watches. A worker often gets one of each.
typeperson (location for registries)A named person, or a radius around a point.
personlastName required; firstName, dobThe name and date of birth you searched at onboarding; the date of birth decides how strong a match can be.
minConfidencedob_match · year_match · age_match · name_match · anyThe weakest match that fires an alert. Unset, it is year_match with a dob and name_match without one.
channelswebhookUrl, email, emails[]Where alerts go. A webhookUrl returns channels.webhookSecret on the 201, once.
labelfree textEchoed 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.

POST /v1/sandbox/monitors/{id}/test-event · the alert a delivery carries
{
  "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
}
Verifying the signature on the raw body
// 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();
HeaderValue
X-Offendersearch-Signaturet=<unix seconds>,v1=<hex>; v1 = HMAC-SHA256(secret, <t>.<raw body>)
Content-Typeapplication/json
User-AgentOffendersearch-Monitor/1
Replay windowRefuse a timestamp more than five minutes from your clock, measured against the header, not the body
SecretPer monitor, whsec_…, shown once on the 201; rotate with POST /v1/monitors/{id}/webhook-secret
FieldTypeWhat it tells you
alert.idstringhit_ + 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.eventTypeenumnew, changed or removed: what happened to the person since the previous evaluation.
alert.matchLabelenumdob_match, year_match, age_match, name_match; null on a removed event.
alert.recordobjectA summary of the record: name, jurisdiction, locality, recordType, externalId and personRef. The full record is one search away.
alert.record.personRefstringThe stable handle for the person. Key your own rows on this and on the alert id, never on a listing number.
alert.firstSeenAt, occurredAtdatetimeWhen the person first matched this monitor, and when this event was filed.
alert.notifiedAtdatetime | nullnull until a delivery succeeded; the history endpoint shows it, so a missed webhook is visible.
webhookDeliveredbooleanSandbox only: whether the test event reached your webhookUrl. Production reports delivery through notifiedAt.
Verify before you parse: a body you parsed before checking the signature is a body an attacker chose.The alert object ›

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.

eventTypeWhen it fires on a criminal monitorWhat the alert carriesWhat to do
newA 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, personRefOpen a review; fetch the record with a search on the personRef’s name and date of birth
changedA 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 summaryRe-read the record; a disposition arriving on an open case is the common case
removedThe person is no longer listed by that source. Reported once.A sparse record: event, jurisdiction, personRef; matchLabel is nullRoute 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.

SituationWhat the platform doesWhat your receiver sees
Endpoint answers 200 in timeSets notifiedAt; no further delivery for this eventOne POST
Endpoint times out or answers 5xxLeaves the alert pending; tries again on the next evaluationThe same (alertId, eventType) again later
200 lost on the networkCannot tell it from a failure; redeliversA duplicate, which the pair-based dedupe discards
Endpoint down for daysAlerts accumulate with notifiedAt: nullEverything, on the first successful evaluation after recovery, or sooner from GET …/alerts
Secret rotatedOld secret stops verifying at once; new one in the 200 onlyDeliveries 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 monitorScheduled re-screen
Latency to a new recordThe next evaluation after the source lists itYour 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 receiveA signed monitor.alert with the deltaThe full result for each row, and the diff is yours to compute
Identity across runspersonRef on the alertresults[].index → personRef per row
Best forActive workers, residents, licenseesWhole rosters, including inactive people
EndpointPOST /v1/monitorsPOST /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.

ItemPriceUnit
Person monitor$3.00per person a month
Location monitor$10.00per location a month
Criminal or registry search, to read a record in full$0.15per call, the first 2,000 in a calendar month
Free to start25 callsonce per account, on production data, no card
Sandbox$060 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.

AspectCheckrOffenderSearch API
Continuous monitoringCheckr 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
Webhookswebhooks signed with HMAC-SHA256 in an X-Checkr-Signature header; retries every minute for 10 minutes, then every hour for 24 hoursAsync 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 forA finished consumer report, per report, by package; court and database fees passed through at costOne API call: scored public records for a name and date of birth, in the same HTTP response
Test environmenta 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.

Path 1

Data API, not FCRA

$0.15a call, 25 free, no card
  • 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.

Path 2

FCRA Partner Access

$4.95per order, searches and live checks included, no court fees
  • 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.

LimitValue
Sandbox, per test key60 calls a minute, 2,000 a day; nothing billed
Production, per keyNo published ceiling. A 429 only when the service is at capacity, with Retry-After; a 429 is never billed
Records per responseUp to 4,000 in one answer; above that capped: true, and perPage pages through the rest
Batch1,000 rows a call; a failing row is reported in place and never stops the batch
DeadlinedeadlineMs bounds the wait for slow jurisdictions; an unanswered one is named in sourceStatus and the answer says partial
EndpointsPOST /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" with product: "criminal" answers 200 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 live block 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.

What a rate-limit answer looks like
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.

Monitor a person with webhooks ›A standing watch on a person, signed alerts, and the receiver that verifies them.
Webhook example ›Receive, verify the signature and retry, in 40 lines of Express or FastAPI.
API signature ›The HMAC scheme behind every webhook we send, with a constant-time check.
Webhook tester ›A hosted endpoint that shows our signed events and verifies them in the browser.
Arrest records API example ›A jail booking as JSON, why a booking is not a conviction, and a monitor for a new one.
Rate limits, retries and idempotency ›429 semantics, backoff that honours Retry-After, and why a repeated POST is a second search.

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.

Background check API ›Criminal records and 58 US sex offender registries on one key, per call.
Criminal records API ›Court cases, jail bookings, prison custody and warrants as JSON.
Sex offender API ›One POST across 58 US registries, scored and cited.
Free background check API ›25 free calls on real data, no card, plus the free sandbox.
Employment screening API ›The public-records stage of a hiring pipeline, and the FCRA path for the decision.
Tenant screening API ›The registry and criminal layer behind a rental application.
Inmate search API ›State prison and county jail custody records.
Background check API pricing ›Costs from 100 to 100,000 searches a month.
Public API for testing ›A free sandbox with documented test people in the production shape.
SDKs: Python, Node, CLI ›Client status and the method surface, one per endpoint.
Postman collection ›Every endpoint with sandbox bodies pre-filled.
MCP server ›Registry and criminal lookups as tools for Claude, Cursor and agents.

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.