Sex Offender Search API
A single authenticated call searches every US sex-offender registry — all 50 states, DC, the territories, and national sources — and returns scored, de-duplicated, source-tagged records. Sync-first for speed; async for guaranteed-complete deep sweeps.
Base URL https://api.offendersearch.comIntroduction
The Offendersearch API is a REST API for national sex-offender screening. It aggregates official state, territory, and national sources behind one endpoint, offers full offenders.io field parity, and adds scored matches, per-source provenance, freshness tiers, and per-registry verification reports. Every request is authenticated with an API key; every response is JSON.
- One call, every registry. Omit
jurisdictionsto sweep all 57 sources, or name a subset. - Sync-first. A typical
POST /v1/searchis a cache read and returns in well under a second. - Provenance built in. Every record links back to the official registry record with a
lastCheckedAt. - Additive contract. New data (e.g. criminal records) lands as a new
recordType— the response shape never breaks.
Quickstart
Create a key in your dashboard, export it, and make your first search. Passing jurisdictions: null (or omitting it) searches all 57 registries at once.
curl https://api.offendersearch.com/v1/search \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": {
"firstName": "John",
"lastName": "Doe",
"dob": "1980-04-12"
},
"jurisdictions": null,
"freshness": "standard",
"match": "balanced",
"include": ["stateData"]
}'Authentication
Authenticate every request with your secret key in the X-API-Key header. Keys are created, named, scoped, rotated, and revoked from the API keys page. A key’s secret is shown in full only once, at creation, and is hashed at rest — store it in a secret manager, never in client-side code.
curl https://api.offendersearch.com/v1/sources \
-H "X-API-Key: os_live_9f2a…"Key scopes
Each key can be scoped to the freshness tiers it may request (standard, weekly, daily) and whether it may generate billable verification reports. A request that exceeds a key’s scope is rejected with 401.
X-Admin-Key internal credential and are not part of the public API documented here.Errors & status codes
The API uses conventional HTTP status codes. Note that a 200 can still be a partial result: when a slow registry can’t beat your deadlineMs, its last-known rows are returned and flagged in sourceStatus rather than failing the whole call. Billing must be enabled on every account before the API returns live results.
| Status | Meaning | When you’ll see it |
|---|---|---|
| 200 | OK | Search completed — or returned partial results under the deadline (check status). |
| 202 | Accepted | Asynchronous search accepted; poll the results URL or await the webhook. |
| 400 | Bad Request | Invalid or missing parameters (e.g. no lastName and no q/location). |
| 401 | Unauthorized | Missing or invalid X-API-Key header, or a request outside the key’s scope. |
| 402 | Payment Required | Billing is not enabled on the account (required for all accounts, including verification reports). |
| 404 | Not Found | The search id or record id does not exist. |
| 429 | Too Many Requests | Rate limit or plan quota exceeded. Back off and retry after the Retry-After header. |
| 504 | Deadline Exceeded | Returned only when the deadline is hit and onDeadline = "error". |
Error response body
Every non-2xx response returns a JSON body with a stable error.code you can switch on and a human-readable error.message.
{
"error": {
"code": "invalid_request",
"message": "query requires at least one of lastName, q, or a lat/lng radius."
}
}Rate limits & quotas
Requests are rate limited per API key. When you exceed your plan’s sustained rate or monthly quota the API responds with 429 Too Many Requests and a Retry-After header (seconds) telling you when to try again. Every response also carries the current limit state so you can throttle proactively.
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Requests allowed in the current window. |
| X-RateLimit-Remaining | Requests left in the current window. |
| X-RateLimit-Reset | Unix time when the window resets. |
| Retry-After | On a 429, seconds to wait before retrying. |
Concurrency and monthly volume are set by your plan. For large batch jobs, prefer the asynchronous endpoint and spread submissions rather than firing thousands of synchronous calls at once. Need a higher ceiling? Contact us and we’ll raise your limits. Handle 429 with exponential backoff that respects Retry-After.
How a search works
Every search moves through the same four stages, so the response is predictable:
- Fan-out. Your
queryis dispatched in parallel to every registry you targeted (all 57 by default, or the codes injurisdictions). - Match & verify. Each registry’s rows are filtered by your
matchmode, then checked against anydob/ageyou supplied to confirm identity. - De-duplicate & score. The same person found in multiple registries is merged into one record whose
sources[]lists every corroborating registry, and each record gets amatchConfidenceandmatchBasis. - Return. You get
records, a per-registrysourceStatus, andcounts— withstatus: "complete"or"partial".
Per-source status
Every registry the sweep touched appears in sourceStatus[] with its own status — so a slow or unavailable jurisdiction is transparent, never a silent gap:
| status | Meaning |
|---|---|
| ok | Queried successfully; matched holds the count from this registry. |
| stale | Cached rows returned because a live refresh could not beat the deadline. Escalate to async for a live read. |
| pending | Still running (async sweeps) — results not yet in. |
| error | The registry failed or timed out; see note. |
| restricted | This registry could not be served for this request; see note. |
| no_coverage | The registry does not cover the queried location. |
Matching & confidence
The match field sets the fuzzy-match tolerance applied uniformly across every registry, so behavior is consistent no matter which states you hit. Whatever mode you choose, providing a dob or age is the strongest lever on accuracy — it verifies identity and pushes matchConfidence toward 1.0.
| Mode | First name | Last name | Use it for |
|---|---|---|---|
| strict | Exact only | Exact only | Lowest false positives; pair with a DOB. |
| balanced | Nicknames, variants, initials, typos (edit-distance 1) | Exact | The default — good recall with controlled noise. |
| broad | All balanced rules | Also prefix + typo tolerance | Maximum recall; expect more low-confidence rows to triage. |
Read matchBasis to see exactly why a record matched (for example ["dob_match", "exact_name"] vs ["surname_only"]) so you can set your own confidence threshold for auto-accept vs manual review.
Freshness tiers
Freshness is a per-call field that sets the maximum acceptable data age. Fresher data means more re-verification on our side, so higher tiers carry a per-call uplift. Every record returns a lastCheckedAt so you can see exactly how current it is.
| Tier | Max data age | Price | Best for |
|---|---|---|---|
| standard | Up to 3 months | $0.10 / search (base only) | Bulk / low-stakes screening |
| weekly | Within 7 days | Base + $0.01 / search | Recurring compliance checks |
| daily | Within 24 hours | Base + $0.035 / search | High-stakes, point-in-time decisions |
Base is $0.10 per search (Standard tier = base only); Weekly freshness adds $0.01 and Daily adds $0.035 per search. See pricing for full details.
Jurisdictions & codes
The jurisdictions array controls which registries run. Omit it or send null to search every registry — the default and common case. Naming codes narrows the sweep to exactly those registries.
jurisdictions: null→ all registries (default).["IL"]→ the Illinois dedicated registry only.["IL", "NSOPW"]→ Illinois and the national NSOPW source.
query.state and jurisdictions are independent. query.state is the subject’s residence — a results filter and a per-scraper hint — and does not change which registries run. Set locationScoped: true with a state to run only registries covering that state plus national sources.Registry codes
National source: NSOPW. State & territory codes are the standard USPS abbreviations:
| Code | Jurisdiction |
|---|---|
| AL | Alabama |
| AK | Alaska |
| AZ | Arizona |
| AR | Arkansas |
| CA | California |
| CO | Colorado |
| CT | Connecticut |
| DE | Delaware |
| DC | District of Columbia (territory) |
| FL | Florida |
| GA | Georgia |
| HI | Hawaii |
| ID | Idaho |
| IL | Illinois |
| IN | Indiana |
| IA | Iowa |
| KS | Kansas |
| KY | Kentucky |
| LA | Louisiana |
| ME | Maine |
| MD | Maryland |
| MA | Massachusetts |
| MI | Michigan |
| MN | Minnesota |
| MS | Mississippi |
| MO | Missouri |
| MT | Montana |
| NE | Nebraska |
| NV | Nevada |
| NH | New Hampshire |
| NJ | New Jersey |
| NM | New Mexico |
| NY | New York |
| NC | North Carolina |
| ND | North Dakota |
| OH | Ohio |
| OK | Oklahoma |
| OR | Oregon |
| PA | Pennsylvania |
| RI | Rhode Island |
| SC | South Carolina |
| SD | South Dakota |
| TN | Tennessee |
| TX | Texas |
| UT | Utah |
| VT | Vermont |
| VA | Virginia |
| WA | Washington |
| WV | West Virginia |
| WI | Wisconsin |
| WY | Wyoming |
| PR | Puerto Rico (territory) |
| GU | Guam (territory) |
| USVI | US Virgin Islands (territory) |
| AS | American Samoa (territory) |
| CNMI | Northern Mariana Islands (territory) |
Sync, async & webhooks
Use POST /v1/search for the fast, common path — it returns within deadlineMs and marks any registry that couldn’t finish as stale. Use POST /v1/searches when you need a guaranteed-complete sweep including slow, browser-gated states; then poll GET /v1/searches/{searchId} or supply a webhookUrl to be notified on completion.
| Synchronous | Asynchronous | |
|---|---|---|
| Endpoint | POST /v1/search | POST /v1/searches |
| Latency | Sub-second typical | Seconds to minutes |
| Completeness | Cached rows for slow sources | Every source, fully |
| Delivery | In the response | Poll or webhook |
| Best for | Interactive checks | Deep sweeps, batch jobs |
Webhooks
Supply a webhookUrl on an async search and we POST the finished SearchResponse to it when the sweep completes — no polling required. Respond 2xx promptly to acknowledge; non-2xx responses are retried with exponential backoff for several attempts. Deliveries are signed so you can verify authenticity.
{
"event": "search.completed",
"searchId": "srch_9f2a7c",
"status": "complete",
"counts": { "records": 3, "sourcesQueried": 57, "sourcesStale": 0 },
"records": [ /* … scored, de-duplicated records … */ ]
}X-Offendersearch-Signature header (an HMAC of the raw request body using your signing secret) before trusting a webhook, and treat delivery as at-least-once — de-duplicate on searchId.Idempotency
Network retries should never start — or bill — a second search. Send an Idempotency-Key header (any unique client-generated string, e.g. a UUID or your order id) on POST /v1/searches. If we’ve already seen that key, we return the original job instead of creating a new one, so a timeout-and-retry is safe.
curl https://api.offendersearch.com/v1/searches \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Idempotency-Key: 7f0c2e14-order-4821" \
-H "Content-Type: application/json" \
-d '{ "query": { "lastName": "Doe" }, "freshness": "daily" }'Reuse the same key when retrying the same logical request; use a fresh key for a genuinely new search. Keys are scoped to your account and retained long enough to cover normal retry windows.
Pagination
POST /v1/search returns the full, de-duplicated result set for a query in a single response — there is no page cursor to manage for a normal identity search, because matches are already scored and merged across registries.
- Broad geographic sweeps. A wide
lat/lng+radiusMilesquery can match many registrants. Narrow the radius or add name filters, or run it as an async search for a guaranteed-complete set. - offenders.io compatibility. The compat endpoint preserves their paged envelope — pass
pageand readpage/totalPagesfrom the response exactly as before.
Verification report
A branded, timestamped PDF/HTML report of your search results that cites the originating registry and links back to the official record — a defensible artifact for your audit file. It is your report of public-record data, clearly labeled as Offendersearch output; it does not reproduce, mirror, or impersonate any government website. It is available to every account and billed per document, for only the registries you explicitly name. Request it inline on a search via the proof field, or after the fact via POST /v1/searches/{searchId}/proof.
{
"registries": ["NJ", "NSOPW"],
"format": "pdf"
}Security & HIPAA
Offendersearch is built for compliance-sensitive customers. All API traffic is served over TLS and data is encrypted at rest; API keys are per-account and stored only as one-way hashes; access to production data is least-privilege; and every request is logged for audit. A Business Associate Agreement (BAA) is available to eligible enterprise accounts that process PHI through the API.
- Encryption. TLS in transit, AES-256 at rest for stored records and reports.
- Per-key hashing. Secrets are hashed at rest — a database read can never expose a usable key.
- Access controls & tenant isolation. Data is scoped per account; one customer can never read another’s keys, usage, or reports.
- Audit logging. Requests are logged with account, timestamp, and endpoint; usage is visible to account owners.
- Honest posture. BAA available and a formal SOC 2 examination is in progress — we don’t claim certifications we don’t hold.
Migrating from offenders.io
Point existing integrations at POST /v1/compat/sexoffender. It accepts offenders.io’s exact parameters and returns their exact { offenders, page, totalPages } envelope — switch by changing only the base URL and key, with no changes to your request or response handling. When you’re ready, move to /v1/search for scored matches, provenance, freshness tiers, and verification reports.
Parameter map
Every offenders.io input has a home in our contract:
| offenders.io | Offendersearch | Notes |
|---|---|---|
| firstName / lastName | query.firstName / query.lastName | Same fields. |
| dob | query.dob | YYYY-MM-DD. Used as a verifier and to boost matchConfidence. |
| city / state / zipcode | query.city / query.state / query.zipcode | Residence filters. |
| address | query.address | Fuzzy street match. |
| lat / lng / radius | query.lat / query.lng / query.radiusMiles | GIS radius (miles, max 100). |
| q | query.q | Free-text across name, aliases, city, ZIP, address. |
| fuzzy: true | match: "balanced" | Fuzzy is a mode with us, applied uniformly across registries. |
| prefixMatch | match: "broad" | Broad adds prefix + typo tolerance on the last name. |
| mode: "extensive" | include: ["stateData"] | Full offenses[], photos, vehicles, state-specific fields. |
| uuid / personUuid | GET /v1/records/{recordId} | Direct record lookup by id. |
| page | page (compat only) | Native /v1/search returns the full de-duplicated set; use async for very large sweeps. |
| createdAt* / updatedAt* filters | source.scrapedAt / source.sourceUpdatedAt | We return these timestamps on every record; date-range request filters are on the roadmap. |
| faceId (Facial Search) | — | Not offered today; on the roadmap. Not part of the current contract. |
What you gain
Moving from compat to /v1/search adds capabilities offenders.io doesn’t offer:
- Scored matches. Every record carries matchConfidence (0–1) and matchBasis. offenders.io returns raw rows with no score.
- Identity verification. dobVerification and unverified tell you exactly how each record was checked against your DOB/age.
- De-duplicated people. One record per person with a sources[] array of every corroborating registry — not repeated rows.
- Per-source status + partial results. sourceStatus reports each registry (ok / stale / error / …) instead of a silent black box.
- Freshness tiers. standard / weekly / daily control data age; every record reports its true lastCheckedAt.
- Verification reports. Branded, timestamped PDF/HTML audit artifacts per registry, on demand.
- Sync-first speed. One blocking call returns scored results — typically sub-second — with async available for deep sweeps.
The Record object
Every match in a search response is a normalized Record. The same schema is returned by /v1/search and /v1/records/{id}. Key fields:
| Field | Type | Description |
|---|---|---|
| recordId optional | string | Stable identifier for the normalized record (use with GET /v1/records). |
| uuid optional | string | Stable per-record UUID (parity with offenders.io uuid / personUuid). |
| recordType optional | enum | sex_offender today; additive as new types ship. |
| matchConfidence optional | number | Our score from 0 to 1. offenders.io returns none. |
| matchBasis optional | string[] | Why it matched: dob_match, exact_name, nickname, age_match, surname_only, … |
| name optional | Name | first, middle, last, suffix, and a pre-formatted full. |
| aliases optional | string[] | Known alternate names and spellings. |
| dob optional | date | null | With dobPrecision: exact · year · year_month · unknown. |
| age optional | string | Captured when a DOB is not published. |
| sex / race / ethnicity optional | string | Demographics where the registry publishes them. |
| height / weight / eyeColor / hairColor / marks optional | string | Physical description. |
| addresses optional | Address[] | residence · employment · school · other, each with line1, city, county, state, zipcode, and optional lat/lng. |
| offense optional | Offense | Primary offense: crime, statute, tier, riskLevel, convictionDate, registrationDate, releaseDate, caseNumber, victim details. |
| offenses optional | Offense[] | The full offense list (returned with include: ["stateData"]). |
| stateData optional | StateData | Extended per-state detail — stateOffenderId, status, designation, registrationEnds, verificationRequirement, lawAgency, judgmentOfConvictionUrl, vehicles[], photos[]. |
| flags optional | object | absconder and predator (nullable booleans). |
| images optional | object[] | Photo URLs where the registry publishes them. |
| source optional | SourceRef | The primary registry: jurisdiction, registryName, recordUrl, scrapedAt, lastCheckedAt, sourceUpdatedAt. |
| sources optional | SourceRef[] | Every registry that corroborated this person (for de-duplicated records). |
| dobVerification optional | enum | How identity was checked: dob_match · age_match · unverified_no_dob_or_age. |
| unverified optional | boolean | True when the record had no DOB and no age to verify against — shown anyway, flagged. |
{
"recordId": "rec_4b1e",
"recordType": "sex_offender",
"matchConfidence": 0.98,
"matchBasis": ["dob_match", "exact_name"],
"name": { "first": "John", "middle": "A", "last": "Doe", "full": "John A. Doe" },
"dob": "1980-04-12",
"dobPrecision": "exact",
"sex": "male",
"addresses": [
{ "type": "residence", "city": "Trenton", "state": "NJ" }
],
"offense": { "tier": "II", "statute": "2C:14-2" },
"flags": { "absconder": false, "predator": false },
"dobVerification": "dob_match",
"unverified": false,
"source": {
"jurisdiction": "NJ",
"registryName": "NJ Sex Offender Internet Registry",
"recordUrl": "https://www.icrimewatch.net/...",
"lastCheckedAt": "2026-07-25T09:14:00Z"
},
"sources": [
{
"jurisdiction": "NJ",
"registryName": "NJ Sex Offender Internet Registry",
"recordUrl": "https://www.icrimewatch.net/...",
"lastCheckedAt": "2026-07-25T09:14:00Z"
}
]
}Synchronous search
The primary endpoint. One authenticated call fans out to every registry (or the ones you name), scores and de-duplicates the matches, and returns them in a single response.
Send a query describing the person you are checking. By default the search covers all 57 registries at once; you can narrow it with jurisdictions, tune the fuzzy-match tolerance with match, control data age with freshness, request extended detail with include, and cap how long you are willing to wait with deadlineMs.
The call is cache/store-first, so a typical request is a read and returns in well under a second. If a slow registry cannot beat your deadline, the response comes back with status: "partial" — the registry’s last-known (cached) rows are still included and flagged in sourceStatus, so you always get an answer rather than a timeout.
Every record you get back carries a matchConfidence score, the matchBasis (why it matched), a dobVerification result, and full per-source provenance with a link to the official registry record and a lastCheckedAt timestamp.
- Search all or some registries. Omit jurisdictions for a nationwide sweep, or pass codes like ["TX","NSOPW"] to scope it.
- Name, DOB, age, or location. Match on any combination — last name or q is the only hard requirement; DOB or age dramatically improves confidence.
- Geographic radius. Provide lat/lng + radiusMiles (or a ZIP/city) to find registrants near a point.
- Free-text and fuzzy street match. Use q for a single free-text field, or address for a fuzzy street-address match.
- Tunable fuzzy matching. Choose strict, balanced, or broad to trade recall for precision uniformly across every registry.
- Extended per-state detail. Add include: ["stateData"] to get offenses[], photos, vehicles, and state-specific fields.
- De-duplicated people. The same person appearing in several registries is merged into one record with a sources[] array.
- Deadline control. Set deadlineMs and onDeadline to bound latency, or let it run to completeness.
| Field | Type | Description |
|---|---|---|
| query required | object | The identity fields to search on. See the query table below. |
| jurisdictions optional · default null | string[] | null | Registry codes to search. null or omitted searches every registry, e.g. ["IL","NSOPW"]. |
| locationScoped optional · default false | boolean | When true and query.state is set, run only registries that cover that state plus national sources. |
| freshness optional · default "standard" | "standard" | "weekly" | "daily" | Maximum acceptable data age. Higher tiers force fresher re-verification (and cost more). |
| match optional · default "balanced" | "strict" | "balanced" | "broad" | Fuzzy-match tolerance applied uniformly across all registries. See Matching & confidence. |
| include optional | string[] | Request extra detail: "stateData" (per-state extended fields, offenses[], photos, vehicles) and/or "raw". |
| recordTypes optional · default ["sex_offender"] | string[] | Which record types to return. Additive as new types ship. |
| proof optional | object | Inline request to generate a verification report. Billed per document. See Verification report. |
| deadlineMs optional · default 120000 | integer | How long to wait, in ms (max 300000). Completeness-first default of 2 minutes; lower it for a fast bounded response. |
| onDeadline optional · default "partial" | "partial" | "error" | On timeout, return partial results (default) or a 504 error. |
lastName (or q, or a lat/lng radius) is the primary key; every other field is an optional filter or verifier.
| Field | Type | Description |
|---|---|---|
| firstName optional | string | Given name. Optional but improves ranking and enables nickname matching. |
| lastName optional | string | Surname — the main search key. |
| dob optional | date (YYYY-MM-DD) | The strongest verifier; a match on DOB pushes confidence toward 1.0. |
| age optional | integer | Used to verify identity when a DOB is not available. |
| city optional | string | Residence city filter. |
| state optional | string | 2-letter code. Filters results to that residence and hints each scraper — does not change which registries run (see jurisdictions). |
| zipcode optional | string | Residence ZIP filter. |
| address optional | string | Fuzzy street-address match. |
| lat / lng optional | number | Coordinates for a radius search. |
| radiusMiles optional · default — | number | Radius around lat/lng (max 100). Returns registrants near the point. |
| q optional | string | Free-text search across name, aliases, city, ZIP, and address in one field. |
curl https://api.offendersearch.com/v1/search \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": {
"firstName": "John",
"lastName": "Doe",
"dob": "1980-04-12"
},
"jurisdictions": null,
"freshness": "standard",
"match": "balanced",
"include": ["stateData"]
}'{
"searchId": "srch_9f2a7c",
"status": "complete",
"freshness": "standard",
"elapsedMs": 142,
"counts": { "records": 1, "sourcesQueried": 57, "sourcesStale": 0 },
"sourceStatus": [
{ "source": "NJ", "status": "ok", "matched": 1, "fromCache": true,
"lastCheckedAt": "2026-07-25T09:14:00Z" },
{ "source": "NSOPW", "status": "ok", "matched": 0, "fromCache": true },
{ "source": "CA", "status": "stale", "matched": 0, "fromCache": true,
"note": "refresh in progress" }
],
"records": [
{
"recordId": "rec_4b1e",
"uuid": "b1e4-…",
"recordType": "sex_offender",
"matchConfidence": 0.98,
"matchBasis": ["dob_match", "exact_name"],
"name": { "first": "John", "middle": "A", "last": "Doe", "full": "John A. Doe" },
"aliases": ["Johnny Doe"],
"dob": "1980-04-12",
"dobPrecision": "exact",
"sex": "male", "race": "white", "height": "5'10\"", "weight": "180",
"addresses": [
{ "type": "residence", "line1": "12 Main St", "city": "Trenton",
"county": "Mercer", "state": "NJ", "zipcode": "08608",
"lat": 40.2171, "lng": -74.7429 }
],
"offense": { "tier": "II", "statute": "2C:14-2", "crime": "Sexual assault",
"registrationDate": "2015-06-01" },
"offenses": [
{ "crime": "Sexual assault", "statute": "2C:14-2", "tier": "II",
"convictionDate": "2014-11-03", "registrationDate": "2015-06-01" }
],
"stateData": {
"stateOffenderId": "NJ-00123", "status": "active", "designation": "Tier II",
"registrationEnds": "2035-06-01", "lawAgency": "Mercer County"
},
"flags": { "absconder": false, "predator": false },
"images": [{ "url": "https://cdn.offendersearch.com/img/rec_4b1e.jpg" }],
"dobVerification": "dob_match",
"unverified": false,
"source": {
"jurisdiction": "NJ",
"registryName": "NJ Sex Offender Internet Registry",
"recordUrl": "https://www.icrimewatch.net/...",
"scrapedAt": "2026-07-25T09:14:00Z",
"lastCheckedAt": "2026-07-25T09:14:00Z",
"sourceUpdatedAt": "2026-07-20T00:00:00Z"
},
"sources": [
{ "jurisdiction": "NJ", "registryName": "NJ Sex Offender Internet Registry",
"recordUrl": "https://www.icrimewatch.net/...",
"lastCheckedAt": "2026-07-25T09:14:00Z" }
]
}
]
}counts summarizes the sweep; sourceStatus reports every registry touched (ok / stale / error / restricted / no_coverage / pending) and whether it was served from cache; records holds the scored, de-duplicated matches.
Pass registry codes in jurisdictions to limit the sweep. Naming ["TX"] hits only the Texas dedicated registry — add "NSOPW" to also include the national source.
{
"query": { "firstName": "Maria", "lastName": "Lopez" },
"jurisdictions": ["TX", "NSOPW"]
}Combine lat/lng with radiusMiles for a geographic sweep — useful for “who lives near this school / address”.
{
"query": {
"lat": 40.7357,
"lng": -74.1724,
"radiusMiles": 5
}
}Pass q to search across name, aliases, city, ZIP, and address in a single field — the offenders.io-style catch-all.
{
"query": { "q": "john doe newark nj" },
"match": "broad"
}Use match: "strict" to require exact name + DOB, and freshness: "daily" for a re-verified, point-in-time answer.
{
"query": { "firstName": "John", "lastName": "Doe", "dob": "1980-04-12" },
"match": "strict",
"freshness": "daily"
}Add include: ["stateData"] to pull the full offenses[] list, photos, vehicles, and state-specific fields (parity with offenders.io extensive mode).
{
"query": { "firstName": "John", "lastName": "Doe" },
"include": ["stateData"]
}Lower deadlineMs and keep onDeadline: "partial" to guarantee a quick answer, accepting that a slow portal may return cached rows marked stale.
{
"query": { "lastName": "Doe" },
"deadlineMs": 4000,
"onDeadline": "partial"
}Asynchronous search
Kicks off a guaranteed-complete sweep and returns immediately with a search id. Use it when completeness matters more than latency — including slow, browser-gated states.
The async endpoint accepts every field the synchronous search accepts, plus an optional webhookUrl. Instead of waiting, it returns 202 Accepted with a searchId and a resultsUrl right away and keeps working in the background until every named jurisdiction has responded.
Retrieve results by polling GET /v1/searches/{searchId} until status is complete, or supply a webhookUrl and we will POST the finished SearchResponse to it — no polling required.
Send an Idempotency-Key header to make retries safe: a repeated key returns the original job instead of starting (and billing) a second search. See Idempotency.
- Guaranteed completeness. Waits for slow browser-gated registries instead of returning them as stale.
- Webhook or poll. Get notified via webhookUrl, or poll the results URL on your own schedule.
- Same query surface. All query fields, jurisdictions, match, freshness, include, and verification-report options carry over.
- Safe retries. An Idempotency-Key header collapses duplicate submissions to one job and one bill.
| Field | Type | Description |
|---|---|---|
| X-API-Key required | string | Your secret API key. |
| Idempotency-Key optional | string | Optional. A unique client token; retries with the same value return the original job. |
| Field | Type | Description |
|---|---|---|
| …SearchRequest required | object | Every field from POST /v1/search is accepted (query, jurisdictions, match, freshness, include, proof, …). |
| webhookUrl optional | uri | We POST the completed SearchResponse here when the sweep finishes. |
curl https://api.offendersearch.com/v1/searches \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f0c2e14-order-4821" \
-d '{
"query": { "lastName": "Doe" },
"jurisdictions": null,
"freshness": "daily",
"webhookUrl": "https://acme.example/hooks/os"
}'{
"searchId": "srch_9f2a7c",
"status": "running",
"jurisdictionsQueried": 57,
"estimatedCompletionSec": 45,
"resultsUrl": "https://api.offendersearch.com/v1/searches/srch_9f2a7c"
}Returns 202 Accepted. status is "pending" or "running"; poll resultsUrl or wait for the webhook.
Get a search
Fetch the current status and results of an asynchronous search.
Returns the same SearchResponse shape as the synchronous endpoint. While the sweep is still running, status is running and records fills in as jurisdictions complete; once every source has reported, status becomes complete.
Poll on your own cadence (a few seconds is typical), or skip polling entirely by supplying a webhookUrl on the original request.
| Field | Type | Description |
|---|---|---|
| searchId required | string | The id returned by POST /v1/searches. |
curl https://api.offendersearch.com/v1/searches/srch_9f2a7c \
-H "X-API-Key: $OFFENDERSEARCH_KEY"{
"searchId": "srch_9f2a7c",
"status": "complete",
"elapsedMs": 41230,
"counts": { "records": 3, "sourcesQueried": 57, "sourcesStale": 0 },
"sourceStatus": [ /* … per-registry status … */ ],
"records": [ /* … scored, de-duplicated records … */ ]
}Returns 404 if the searchId does not exist. status is running until every jurisdiction reports.
Generate a verification report
A branded, timestamped PDF/HTML report of your search results that cites the originating registry and links back to the official record — a defensible artifact for your audit file. It is your report of public-record data, clearly labeled as Offendersearch output; it does not reproduce, mirror, or impersonate any government website.
A verification report is generated for only the registries you name in registries (the API field is still called proof), and each document is billed separately. It is available to every account (billing must be enabled). Omit it entirely and there is no extra charge.
You can request a verification report inline on a search via the proof field, or after the fact against a completed searchId with this endpoint.
- Per-registry, on demand. Name exactly which registries to include — never all sources implicitly.
- PDF or HTML. Choose the format that fits your audit workflow.
- Audit-ready. Each report is clearly labeled as Offendersearch output, cites the originating registry, and links back to the official record.
| Field | Type | Description |
|---|---|---|
| searchId required | string | The search to generate a verification report for. |
| Field | Type | Description |
|---|---|---|
| registries required | string[] | Registry codes to render (min 1), e.g. ["IL","NSOPW"]. Each document is billed. |
| format optional · default "pdf" | "pdf" | "html" | Output format for every document in the bundle. |
curl https://api.offendersearch.com/v1/searches/srch_9f2a7c/proof \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{ "registries": ["NJ", "NSOPW"], "format": "pdf" }'{
"status": "ready",
"billedDocuments": 2,
"documents": [
{ "registry": "NJ", "format": "pdf", "url": "https://files.offendersearch.com/proof/nj-….pdf" },
{ "registry": "NSOPW", "format": "pdf", "url": "https://files.offendersearch.com/proof/nsopw-….pdf" }
]
}Returns 402 Payment Required if billing is not enabled on the account.
Get a record
Fetch a single normalized record by id — for re-displaying or refreshing a specific person you already found.
Use the recordId (or uuid) returned in a search result — this is our equivalent of offenders.io’s uuid / personUuid lookup. Returns the full Record object, including every source that corroborated the person.
| Field | Type | Description |
|---|---|---|
| recordId required | string | The recordId (or uuid) from a search result. |
curl https://api.offendersearch.com/v1/records/rec_4b1e \
-H "X-API-Key: $OFFENDERSEARCH_KEY"{
"recordId": "rec_4b1e",
"uuid": "b1e4-…",
"recordType": "sex_offender",
"name": { "first": "John", "middle": "A", "last": "Doe", "full": "John A. Doe" },
"dob": "1980-04-12",
"dobPrecision": "exact",
"dobVerification": "dob_match",
"unverified": false,
"source": {
"jurisdiction": "NJ",
"registryName": "NJ Sex Offender Internet Registry",
"recordUrl": "https://www.icrimewatch.net/...",
"lastCheckedAt": "2026-07-25T09:14:00Z"
}
}Returns 404 if the recordId does not exist.
List sources
The coverage catalog: every jurisdiction we cover, by code, name, scope, and live health.
A public, keyed view of every jurisdiction we cover, with its code, display name, and scope (state or national), plus lightweight health signals such as typical latency and last successful refresh. Use it to render your own coverage UI or to decide which jurisdictions to name.
- Coverage. Every jurisdiction with its code, name, and scope (state/national).
- Health. Typical latency and last-success timestamps so you can see what’s live.
curl https://api.offendersearch.com/v1/sources \
-H "X-API-Key: $OFFENDERSEARCH_KEY"[
{
"id": "NJ",
"name": "NJ Sex Offender Internet Registry",
"covers": ["NJ"],
"scope": "state",
"health": { "lastSuccessAt": "2026-07-25T09:14:00Z", "typicalLatencyMs": 380 }
},
{
"id": "NSOPW",
"name": "National Sex Offender Public Website",
"covers": ["US"],
"scope": "national"
}
]offenders.io compatibility
A drop-in endpoint that accepts offenders.io’s exact parameters and returns their exact { offenders, page, totalPages } envelope.
Point an existing offenders.io integration here and it keeps working by changing only the base URL and API key — no code changes to your request or response handling.
Under the hood it maps onto the same engine, so you can migrate incrementally: run on compat today, then move to /v1/search when you want scored matches, provenance, and freshness tiers. See Migrating from offenders.io for the full parameter map.
| Field | Type | Description |
|---|---|---|
| firstName / lastName optional | string | Name fields. |
| dob / city / state / zipcode optional | string | Standard filters. |
| address optional | string | Fuzzy street match. |
| lat / lng / radius optional | number | GIS radius search (radius in miles, max 100). |
| q optional | string | Free-text query. |
| fuzzy optional | boolean | Enable fuzzy name matching (maps to match: "balanced"). |
| mode optional | "extensive" | Request extended per-state detail (maps to include: ["stateData"]). |
| prefixMatch optional | "firstName" | "lastName" | Prefix-match a name field (maps to match: "broad"). |
| uuid / personUuid optional | string | Direct record lookup (maps to GET /v1/records/{id}). |
| page optional | integer | Page number for the paginated envelope. |
curl https://api.offendersearch.com/v1/compat/sexoffender \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-H "Content-Type: application/json" \
-d '{ "firstName": "John", "lastName": "Doe", "state": "NJ", "fuzzy": true, "mode": "extensive" }'{
"offenders": [ /* offenders.io-shaped records */ ],
"page": 1,
"totalPages": 1
}The response is the offenders.io envelope, returned verbatim. Move to /v1/search for scored matches, provenance, freshness, and verification reports.