Every API here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for apis
7 MCP tools reach this
find_apisBrowse and filter every API in the catalog.
get_api_artifactsOne API's artifacts, grouped by type.
get_openapiThe primary OpenAPI for this API.
find_similar_apisAPIs that look like this one.
apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
resolveTurn a domain, URL or GitHub org into the provider it belongs to.
find_cohortsEvery scored population of providers in the catalog.
All 92 tools →
Call it yourself
curl for this page
This API
curl "https://apis.io/api/v1/apis/offendersearch-api-report-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we
store it to create your key and to recognise you if you sign in with another
provider. See our Privacy Policy and
Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Offendersearch Report API
version: 1.0.0
description: National sex-offender search across all US states and territories, unified behind one API.
servers:
- url: https://api.offendersearch.app
security:
- ApiKeyAuth: []
tags:
- name: Report
paths:
/v1/report:
post:
operationId: makeReport
summary: Consolidated verification / proof PDF for one search
description: 'Renders a SINGLE consolidated verification PDF for a search that was ALREADY run — pass the `searchId` of a prior `POST /v1/search` (or `/v1/searches`) call (valid for 7 days), optionally with the `viewerName`/`viewerEmail` of whoever is viewing it (both OPTIONAL, printed at the top as a viewer verification — not an access gate). Any valid key or session may call it — there is NO per-key entitlement. Because the search call was already billed, the report meters ONLY the **+$0.02 per-PDF** charge (admin-overridable per customer via `pdf_rate_cents`). (Legacy one-shot: omit `searchId` and pass an inline `query`, which runs the search inline and also bills the call.) The PDF shows EVERY matching offender and EVERY field on file (name, aliases, DOB/age match state, address/jurisdiction, offense/risk, photo when available, and the full extensive field set), plus a source-attribution section with a citation on every record — proving provenance. A legal notice (use restrictions; informational only, not an FCRA consumer report) is printed on the document. Responds with `application/pdf` as a file attachment (`Content-Disposition: attachment`); the report id is echoed in the `X-Report-Id` header. Accepts an API key OR a session token.'
security:
- ApiKeyAuth: []
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ReportRequest'
responses:
'200':
description: The consolidated verification report (a PDF file attachment).
headers:
X-Report-Id:
description: Unique id of the generated report (also printed on the document).
schema:
type: string
example: rpt_9f2c1a7b3e4d
content:
application/pdf:
schema:
type: string
format: binary
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/Unprocessable'
tags:
- Report
components:
schemas:
ReportRequest:
description: 'Generate a verification report for a search that was ALREADY run: pass the `searchId` returned by a prior `POST /v1/search` (or `/v1/searches`) call — up to 7 days afterward. The viewer identity (`viewerName`/`viewerEmail`, or the legacy `requesterName`) is OPTIONAL and is printed at the top of the PDF as a record of who viewed it — it is NOT an access gate. A legacy one-shot mode is still supported: omit `searchId` and pass an inline `query` (+ optional `jurisdictions`/`locationScoped`/`freshness`) to run the search inline. Provide EITHER a `searchId` OR a `query`.
'
type: object
properties:
searchId:
type: string
nullable: true
description: Id of a prior /v1/search (or /v1/searches) call to report on (within the 7-day window).
example: srch_9f2c1a7b3e4d
query:
$ref: '#/components/schemas/Query'
jurisdictions:
type: array
nullable: true
items:
type: string
description: Legacy one-shot mode only (ignored when searchId is given).
locationScoped:
type: boolean
default: false
freshness:
type: string
enum:
- daily
- weekly
default: daily
viewerName:
type: string
nullable: true
description: OPTIONAL — name of the person viewing/running the report (printed on the PDF).
example: Jane Doe, HR Compliance
viewerEmail:
type: string
nullable: true
description: OPTIONAL — email of the viewer (printed on the PDF).
example: jane@acme.example
requesterName:
type: string
nullable: true
description: Legacy alias for viewerName (optional).
purpose:
type: string
nullable: true
description: Optional stated purpose for the lookup (printed on the PDF).
example: Volunteer background screening
reference:
type: string
nullable: true
description: Optional caller reference / case id (printed on the PDF).
example: case-2026-0417
Error:
type: object
description: Standard FastAPI error body.
properties:
detail:
type: string
description: Human-readable error message.
example: Missing or invalid API key. Send it in the X-API-Key header.
Query:
type: object
additionalProperties: false
description: 'The person/location query. All fields are optional; supply what you have. DOB/age drive the match-state (see Record.dobVerification). offenders.io parity filters (q/address/prefixMatch/fuzzy/createdAt*/updatedAt*/page/ perPage) are applied uniformly across ALL jurisdictions.
'
properties:
firstName:
type: string
description: Given name.
example: John
lastName:
type: string
description: Surname (primary match key).
example: Smith
dob:
type: string
format: date
description: 'Date of birth, `YYYY-MM-DD`. Optional, and it NARROWS — a record whose published birth evidence conflicts with this date is excluded.
★ **It does not require us to hold a date of birth for the person.** The date you send is matched against every kind of birth evidence a registry publishes: a full date, a birth year alone, or a published age alone. Records with none of those are still returned on the name, flagged unverified. `matchState` on each record tells you which of those happened and how strong it was — read it there; the four outcomes are `dob_match`, `year_match`, `age_match` and `no_dob_age_year`.
'
example: '1985-06-14'
age:
type: integer
description: Age filter. Used when DOB is unavailable; matched ±1 year for birthday drift.
example: 39
city:
type: string
description: Residence city filter.
example: Chicago
state:
type: string
description: '2-letter USPS state/territory code (`FL`), case-insensitive; the full state or territory name (`Florida`) is also accepted and means exactly the same thing. A value we cannot resolve to a jurisdiction is rejected with 422 — it is never silently treated as `matches nothing`.
★ IT IS A UNION, AND YOU SHOULD KNOW WHICH HALF MATCHED. `state` keeps a record when EITHER one of its `addressStates` is that state (they live there) OR its `registrationState` is (that state''s registry holds them). It does NOT change which registries run — all 58 are searched and `state` narrows the answer. (Until 2026-08-05 it silently scoped the fan-out to that state''s own registries, which hid anyone registered elsewhere while residing there; use `locationScoped: true` if you explicitly want the cheaper, narrower search.) Those two halves are genuinely different populations: 105,028 records are registered in a state where they have no address on file, and 70,973 records carry no address state at all and are reachable ONLY by the registration half. Every record comes back carrying both `registrationState` and `addressStates`, so you can tell which half answered without a second call — narrow to residents with `addressStates`, or to a registry''s roster with `jurisdictions: ["FL"]`, which is the registration-only filter.
'
example: IL
zipcode:
type: string
description: Residence ZIP (first 5 used).
example: '60614'
address:
type: string
description: 'Parity: fuzzy street-address match (every token must appear in some record address).'
lat:
type: number
description: Latitude for GIS radius search.
example: 41.9
lng:
type: number
description: Longitude for GIS radius search.
example: -87.65
radiusMiles:
type: number
maximum: 100
description: 'Parity: GIS radius in miles. Defaults to 1 when lat/lng given; capped at 100.'
q:
type: string
description: 'Parity: free-text across name/alias/address/city/state/zip (all tokens must appear).'
fuzzy:
type: boolean
description: 'Parity: offenders.io fuzzy toggle. true -> balanced match; false -> strict. Overrides `SearchRequest.match`.'
prefixMatch:
description: 'PARTIAL-NAME SEARCH. Treat the name(s) you supplied as the START of a name: `thom` returns Thomas, Thompson and Thomason. Accepts "firstName", "lastName", "both" (prefix-match BOTH fields at once), or a list. Matched against every recorded ALIAS as well as the registered legal name; `matchedName` on each record says which one matched. MINIMUM 3 CHARACTERS — a shorter prefix returns 422. Prefix matching is a strict superset of exact matching, and an exact match always ranks above a prefix match. `nameMatch` overrides this.
'
oneOf:
- type: string
enum:
- firstName
- lastName
- both
- type: array
items:
type: string
enum:
- firstName
- lastName
nameMatch:
type: object
description: 'EXPLICIT PER-FIELD CONTROL over how names are matched — the fine-grained form of `prefixMatch`/`match`. Anything omitted from a field''s list is OFF for that field. Exact matching is always on and cannot be disabled. Overrides both `prefixMatch` and `SearchRequest.match`.
'
properties:
firstName:
type: array
description: Strategies for the first name. prefix requires >=3 characters; middle requires >=2.
items:
type: string
enum:
- exact
- prefix
- nickname
- fuzzy
- middle
lastName:
type: array
description: Strategies for the last name. prefix requires >=3 characters.
items:
type: string
enum:
- exact
- prefix
- fuzzy
aliases:
type: boolean
default: true
description: Also apply these strategies to every recorded alias.
faceId:
type: string
description: Parity input ONLY — facial search is NOT supported; supplying it returns 422 (never a faked face match).
createdAtStart:
type: string
format: date-time
description: 'Parity: lower bound on source.scraped_at (when we first recorded the record).'
createdAtEnd:
type: string
format: date-time
description: 'Parity: upper bound on source.scraped_at.'
updatedAtStart:
type: string
format: date-time
description: 'Parity: lower bound on source.source_updated_at (when the source last changed the record).'
updatedAtEnd:
type: string
format: date-time
description: 'Parity: upper bound on source.source_updated_at.'
page:
type: integer
minimum: 1
description: 'Parity: 1-based page number. Omit it and the whole match set is returned in a single page — up to the response cap. ★ ABOVE THE CAP, OMITTING IT IS NOT THE SAME AS ASKING FOR EVERYTHING: an unpaginated answer larger than `cappedLimit` (4,000) is trimmed to that many records and says so with `capped: true`, and `cappedOmittedSources` names the registries that contributed nothing to it. A PAGINATED request is never trimmed — it walks the entire match set and `capped` stays `false`. So `counts.records` vs `counts.recordsReturned` is the pair to branch on: if they differ, send `perPage` and page through. Most searches never reach this — a `name + DOB` query returns tens of records — but a bare common surname does. A page past `totalPages` returns an empty `records` array (changed 2026-08-05; it used to re-serve the last page).
'
perPage:
type: integer
minimum: 1
description: 'Parity: page size. Defaults to 20 when paginating. Sending it turns pagination ON, which is what makes an over-cap match set fully reachable. Clamped to `cappedLimit`, so a single page can never rebuild the oversized response the cap exists to prevent.
'
ageTolerance:
type: integer
minimum: 0
maximum: 10
default: 1
description: 'How many years of slack the age comparison allows, when the query carries a `dob` and the record publishes only an `age`. Eleven registries publish an age and no date at all, so a `name + DOB` search has to compare a searched DATE against a published AGE; how much slack that allows is a risk decision, and it is yours. The default of 1 is not arbitrary — a published age with an unpublished birthday is consistent with two birth years, and the registry may have computed it a refresh before we read it, so the age is anchored to the date we read that page rather than to today. Raise it for a high-recall screening pass: more same-name strangers, fewer missed true matches.
'
onAgeMismatch:
type: string
enum:
- drop
- flag
default: drop
description: 'What to do with a record that matches on NAME but whose published age contradicts the searched `dob`. `drop` (default) omits it. `flag` RETURNS it, labelled `matchState: "age_mismatch"`, and lets you judge — "silently omitted" and "checked, and the age contradicts your date" are different facts. A flagged record is always `unverified` and can never be reported as DOB-confirmed.
'
responses:
Unauthorized:
description: Missing or invalid credential.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unprocessable:
description: 'Unprocessable request — e.g. `faceId` supplied (facial search is not supported; we never fake a face match).
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: Customer API key. Primary auth for search/records/compat/proof.
BearerAuth:
type: http
scheme: bearer
description: 'Signed session token (HMAC-SHA256). Auth for account/dashboard endpoints; also accepted by POST /v1/search and the compat endpoint.
'
QueryKeyAuth:
type: apiKey
in: query
name: key
description: API key passed as `?key=` — offenders.io demo mode (compat endpoint only).
AdminAuth:
type: apiKey
in: header
name: X-Admin-Key
description: Internal admin credential — separate from customer API keys.