Offendersearch
API Reference · v1.0.0

Migrating from another provider

A drop-in compatibility endpoint, the full legacy parameter map, and what /v1/search adds.

Base URL https://api.offendersearch.appThis page as Markdown/docs/migration.md

Switch by changing the base URL

Point existing integrations at POST /v1/compat/sexoffender. It mirrors legacy sex-offender search APIs’ exact parameters and returns the exact { offenders, page, totalPages } envelope — so you switch by changing only the base URL and key, with no changes to your request or response handling. When you are ready, move to /v1/search for scored matches, per-source status, freshness tiers, and verification reports.

The whole migration, in two lines
- const BASE = "https://api.previous-provider.example";
+ const BASE = "https://api.offendersearch.app/v1/compat";
- headers: { "Authorization": "Bearer " + LEGACY_KEY }
+ headers: { "X-API-Key": process.env.OFFENDERSEARCH_KEY }

Parameter map

Every legacy input has a home in our contract:

Legacy APIOffendersearchNotes
firstName / lastNamequery.firstName / query.lastNameSame fields.
dobquery.dobYYYY-MM-DD. Used as a verifier and to boost matchConfidence.
city / state / zipcodequery.city / query.state / query.zipcodeLocation filters. query.state is a UNION — it keeps a record with an address in that state OR held by that state's registry. Read registrationState / addressStates on each record to tell which.
addressquery.addressFuzzy street match.
lat / lng / radiusquery.lat / query.lng / query.radiusMilesGIS radius (miles, max 100).
qquery.qFree-text across name, aliases, city, ZIP, address.
fuzzy: truematch: "balanced"Fuzzy is a mode with us, applied uniformly across registries.
prefixMatchquery.prefixMatchSame field, and it also accepts "both" to prefix-match first and last name together. Ours matches aliases as well as the registered name, and reports which one matched.
mode: "extensive"include: ["stateData"]Full offenses[], photos, vehicles, state-specific fields.
uuid / personUuidGET /v1/records/{recordId}Direct record lookup by id.
pagepage / perPageNative /v1/search returns the full de-duplicated set in one response, up to a defined 4,000-record response cap. An unpaginated answer above that cap is returned with capped: true; send perPage to paginate and every matched record is reachable, with capped false. Branch on counts.records vs counts.recordsReturned: if they differ, there is more to fetch.
createdAt* / updatedAt* filtersquery.createdAtStart/End · query.updatedAtStart/EndInclusive range bounds on source.scrapedAt and source.sourceUpdatedAt, both of which are returned on every record.
faceId (Facial Search)Not part of the current contract.

What you gain

Moving from compat to /v1/search adds capabilities legacy providers do not offer:

  • Scored matches. Every record carries matchConfidence (0–1) and matchBasis. Legacy APIs return 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. sourceStatus reports every jurisdiction the request touched (ok / error / restricted / …) on every response, so an incomplete search is always labelled.
  • Freshness tiers. daily is the most current tier; weekly is one tier behind at no surcharge. Every record reports its own lastCheckedAt on either tier.
  • Verification reports. A branded, timestamped PDF of the full search results with a source citation on every record — one consolidated document per search, on demand.
  • Sync-first speed. One blocking call returns scored results in a single round trip, with elapsedMs on every response, and an async endpoint for unbounded work.
One difference worth planning for. The legacy envelope has room for a single completeness signal — the integer error: 503 — with no reason and no per-source detail. On /v1/search the same condition arrives as status: "partial" with a full sourceStatus[] naming each jurisdiction and why it did not complete. That is the main reason to finish the migration rather than stop at compat.

A staged migration

  1. Swap the host and key. Point at /v1/compat/sexoffender and run your existing test suite unchanged. The response envelope is identical.
  2. Shadow-read /v1/search. Issue the same query to the native endpoint alongside compat and diff the record sets. Key the diff on sources[].recordUrl with its jurisdiction — not on recordId, which is derived from the merge for a given query scope.
  3. Adopt the labelled fields. Branch on matchState and matchDetail.strategies to set your own auto-accept threshold, and on counts.sourcesIncomplete to separate no match from not determined.
  4. Cut over. Move production traffic to /v1/search, and keep compat available for any integration you have not migrated yet — both endpoints stay supported.

Generating a client rather than hand-writing one? The full OpenAPI document is published at /openapi.json and /openapi.yaml, and it is the same specification these pages are written against.

POST/v1/compat/sexoffenderAuth: X-API-Key

Compatibility endpoint

A drop-in endpoint that mirrors legacy sex-offender search APIs’ exact parameters and returns the exact { offenders, page, totalPages } envelope.

Point an existing legacy 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 another provider for the full parameter map.

Combination rules (return 400 with {code, message}): q cannot be combined with firstName/lastName or with lat/lng; address cannot be combined with q or with lat/lng.

GIS search (lat + lng): results page 50 per page (regular searches page 20), and the query defaults to the last 90 days of source updates unless you pass an explicit updatedAtStart. A missing radius defaults to 1 mile (max 100).

Parameters · Body (legacy parameters, verbatim)
FieldTypeDescription
firstName / lastName
optional
stringName fields. Cannot be combined with q (400).
dob / city / state / zipcode
optional
stringStandard filters.
address
optional
stringFuzzy street match. Cannot be combined with q or lat/lng (400).
lat / lng / radius
optional
numberGIS radius search (radius in miles, max 100). GIS pages 50/page and defaults to the last 90 days of updates.
q
optional
stringFree-text query. Cannot be combined with firstName/lastName or lat/lng (400).
fuzzy
optional
booleanEnable fuzzy name matching (maps to match: "balanced").
mode
optional
"extensive"Request extended per-state detail (maps to include: ["stateData"]).
prefixMatch
optional
"firstName" | "lastName" | "both"Prefix-match a name field — the name you send is treated as the start of a name (minimum 3 characters), matched against aliases too. See Partial name search.
createdAtStart / createdAtEnd
optional
date-timeRange filter on when we first recorded the record (source.scrapedAt).
updatedAtStart / updatedAtEnd
optional
date-timeRange filter on when the source last changed the record; GIS defaults updatedAtStart to now − 90 days.
uuid / personUuid
optional
stringDirect record lookup (maps to GET /v1/records/{id}).
page
optional
integerPage number for the paginated envelope.
Request
curl https://api.offendersearch.app/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" }'
Response
200 OK
{
  "offenders": [ /* legacy-shaped records */ ],
  "page": 1,
  "totalPages": 1
}

The response is the legacy envelope, returned verbatim. Move to /v1/search for scored matches, provenance, freshness, and verification reports.