Sex offender GeoJSON, ready for a map layer.
Sex offender GeoJSON for whatever area your map is showing. Add format=geojson to a Map API request and the answer is a FeatureCollection of points: clusters when zoomed out, pins closer in, names close up. Hand it to a map source as it is.
$2.00 per 1,000 map requests · $0.015 per record opened · $300 monthly minimum · Test mode is free
curl -G https://api.offendersearch.app/v1/map/viewport \
-H "X-API-Key: $OFFENDERSEARCH_KEY" \
-d bbox=-88.3,41.5,-87.2,42.2 \
-d zoom=9 \
-d format=geojson \
-d sandbox=trueWhat sex offender GeoJSON looks like
One FeatureCollection per map view. Every feature is a Point, with coordinates in GeoJSON order: longitude first.
Cluster features
Below zoom 13. A count for an area, with cluster: true and the property names cluster layers already read.
Pin features
From zoom 13. One feature per registered address, with kind: "marker". From zoom 15 it carries a label: first initial and last name.
Area features
kind: "area". People placed only to the middle of a ZIP code, city or county. Style them differently from an exact pin.
Everything that is not a feature is under meta: the mode, the zoom used, the total, the count of people with no mappable address, and the notice to show on the map. The content type is application/geo+json.
The property names on a cluster feature
They are the names MapLibre and Mapbox GL cluster layers style, so an existing cluster layer works without changes.
| Property | Type | What it holds |
|---|---|---|
cluster | true | Marks the feature as a cluster. Pins and areas do not have it. |
cluster_id | string | The id of the cluster. Use it as a key. |
point_count | number | People in the cluster. Also the weight for a heat map layer. |
point_count_abbreviated | string | The count shortened for a label: 412, 1.8k, 12k. |
expansion_zoom | number | The zoom at which the cluster comes apart. |
bbox | array | West, south, east, north: the extent of the people in the cluster. |
breakdown_street | number | Of the count, people at an exact street address. |
breakdown_housed | number | Housing status, where the registry states it. |
breakdown_homeless | number | As above. |
breakdown_incarcerated | number | As above. |
The breakdown is flat on purpose. A style expression can read ["get", "breakdown_homeless"] directly, so a filtered count needs no second request.
Marker, group and area features
Tell them apart with kind. The other properties are the same fields the JSON format returns.
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-87.62981, 41.88123]
},
"properties": {
"kind": "marker",
"id": "mk_Zb3…",
"precision": "street",
"addressType": "home",
"label": "J. Smith",
"housingStatus": "housed",
"hasPhoto": true,
"registryState": "IL"
}
}- marker: one person at one address. Its
idopens the full record. - group: several people at one address.
idis null;groupIdlists who is there. - area: a count at the middle of a ZIP code, city or county. Never draw it as an exact address.
A view with more than 300 pins is answered as cluster features on a finer grid, so one collection never grows past what a layer draws smoothly.
Draw sex offender GeoJSON on a MapLibre map
A source, three layers and one function that asks again when the map stops moving. Mapbox GL takes the same code.
import { Map } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
// "/api/map/viewport" is a route on YOUR server.
// It adds the secret key and passes the request on.
const map = new Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [-87.6298, 41.8781],
zoom: 10,
});
map.on('load', () => {
// The clusters arrive ready-made: no "cluster: true" here.
map.addSource('people', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
});
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'people',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#1d4ed8',
'circle-radius': ['step', ['get', 'point_count'], 16, 50, 22, 500, 30],
},
});
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'people',
filter: ['has', 'point_count'],
layout: {
'text-field': ['get', 'point_count_abbreviated'],
'text-font': ['Noto Sans Regular'],
'text-size': 12,
},
paint: { 'text-color': '#ffffff' },
});
map.addLayer({
id: 'pins',
type: 'circle',
source: 'people',
filter: ['!', ['has', 'point_count']],
paint: { 'circle-color': '#b91c1c', 'circle-radius': 6 },
});
refresh();
});
map.on('moveend', refresh);
async function refresh() {
const b = map.getBounds();
const query = new URLSearchParams({
bbox: [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()].join(','),
zoom: String(Math.floor(map.getZoom())),
format: 'geojson',
});
const res = await fetch('/api/map/viewport?' + query);
if (res.ok) map.getSource('people').setData(await res.json());
}The source is a plain GeoJSON source. Leave the library’s own clustering off: the features are already clustered for the zoom you sent, and counting them again would group clusters of clusters.
A click on a cluster fits the map to its bbox at its expansion_zoom. The next moveend then asks for the new view.
map.on('click', 'clusters', (e) => {
const p = e.features[0].properties;
// MapLibre hands array properties back as JSON text.
const [west, south, east, north] = JSON.parse(p.bbox);
const cam = map.cameraForBounds([[west, south], [east, north]], {
padding: 48,
});
map.easeTo({
center: cam.center,
zoom: Math.max(p.expansion_zoom, Math.min(cam.zoom, 17)),
});
});The complete map, with popups, the many-people list and the record panel, is built step by step in how to build a sex offender map.
Leaflet, Google Maps and heat maps
Leaflet
Pass the collection to L.geoJSON and choose the marker in pointToLayer from properties.cluster and properties.kind.
Google Maps
map.data.addGeoJson takes it as it is. Set the icon in map.data.setStyle with a function of the feature.
Heat maps
Feed the cluster features to a heat map layer and use point_count as the weight. No pins are needed for it.
A view at a time, not a file
The GeoJSON is an answer for one map view. It is not a download of a state or of the country. That is what keeps a map quick: a view of a whole region is a few dozen cluster features, and a street is a few pins.
It is also the licence. Map data is for display in your own application, and a result may be cached for a short time, not kept as a copy. The response says so in the fields it sends: meta.notice is the line to show beside the map. “A pin shows a registered address, not a person's current location.”
The format costs nothing extra. A map request is one answer for one map view. It costs the same whether the view holds three people or three hundred. Prices, the monthly minimum and how to turn the Map API on are on the sex offender map API page.
Sex offender GeoJSON: common questions
Is there a sex offender GeoJSON file I can download?
Not as a file. The GeoJSON is returned per map view by the Map API, so your map always asks for the area on screen and gets clusters or pins for it. A whole-state or whole-country file is not offered.
Which registries does the GeoJSON cover?
58 US registries: the 50 states, DC and the US territories. One request covers whatever part of them is in view.
Which way round are the coordinates?
Longitude first, then latitude, as the GeoJSON standard requires. The bbox property is west, south, east, north.
Should I turn on clustering in my map library?
No. The features are already clustered for the zoom you sent, and each cluster carries its count. Leave the library’s own clustering off and style on point_count.
Why is the bbox property a string when I read it from a clicked feature?
MapLibre and Mapbox GL turn array and object properties into JSON text on rendered features. Parse it with JSON.parse before use. The response itself holds a real array.
Does the GeoJSON include names?
From zoom 15, each pin has a label: a first initial and a last name. Wider views hold positions and counts only. The full record comes from a separate request when a pin is opened.
Does GeoJSON cost more than JSON?
No. Either format is one map request: $2.00 per 1,000 map requests.
Can I try it without paying?
Test mode returns made-up people in the real response shape. Add sandbox=true to any map request. It needs no plan and is never charged.
How current is the data?
It is the same continuously updated registry data the search API returns. A record opened from the map carries its own lastCheckedAt.
Give your map a feature collection
Build against test mode today, then turn the Map API on when the map is ready for real records.
The Offendersearch APIs
One account, one key, one bill — the sex offender registry and criminal records, side by side.
Sex offender registry
- Sex offender API — One call across 58 US sex offender registries, with scored matches and a citation on every record.
- Sex offender search API — Endpoints, request shapes, record fields, matching and batch, in detail.
- Sex offender database — The national registry data in one schema — photos, aliases, offenses, addresses.
- Registered offender database — What a registered offender record holds, and how its accuracy is measured.
- Bulk sex offender search — Screen a whole list of names in one upload or one API call.
- Sex offender registry API — Choose which of the 58 registries a search covers, and see which ones answered.
- Offender search API — The registry search and the criminal search side by side, on one key.
Criminal records
- Criminal records API — County, state and federal records — jail, prison, court and warrant — through one API.
- Criminal background check API — One cited answer per person for onboarding and screening flows.
- Criminal search API — The search endpoint itself: parameters, record types and coverage, call by call.
- Court records API — Criminal court cases with charges and a disposition on each charge.
- Arrest records API — County jail bookings: booking date, custody status, facility and photo link.
- Inmate search API — State prison and county jail custody, with sentence, release and parole dates.
- Background check API — The registry and criminal layers together, on one key and one bill.
- Employment background check API — The registry and criminal data layer for HR, ATS and screening platforms.
- Employment screening API — The stages of a hiring pipeline, and where criminal and registry checks sit in it.
- Bulk criminal record search — Run a list of names against criminal records in one call.
- Statewide criminal search — One state at a time: prison, court and warrant systems that cover the whole state.
- Federal criminal records — Federal criminal records through the same API, and what a federal search covers.
Build with it
- API documentation — Quickstart, every endpoint, the record object and the OpenAPI spec.
- Sex offender map API — Clusters, pins and names for your own map, and the full record when one is opened.
- Background check API pricing — $0.15 a call, $0.11 past 2,000 a month, 25 free calls to start — worked out by volume.
- Background check API integration — Keys, the first call, async and webhooks, batch, monitoring and errors, step by step.
- Free background check API — 25 free calls on real data, no card — what is included and what comes after.
- Pay-as-you-go background check API — Per call, per report or subscription: the pricing models, with costs by volume.
- Background check API in Python — A working tutorial with requests: search, batch, async, errors and retries.
- Background check API in Node.js — A fetch client, response types, an Express webhook handler and retries.
- NSOPW API — Is there one? What exists, and a developer API for searching US registries.
- MCP server for offender search — Give Claude and other AI agents the same search as a tool.
- Integrations — Screening inside referral software — CarePort, ExtendedCare, Ensocare and Aidin.
- AI integration — Offender safety checks for AI agents and chatbots, with cited answers.
- Offendersearch vs offenders.io — A side-by-side comparison of the two registry APIs.
Screening and compliance
- Tenant background check — Screening applicants for a rental, and where an FCRA workflow is required.
- FCRA Partner Access — The FCRA workflow for hiring and tenant decisions — pricing by quote.
- FCRA-compliant background check data — Accuracy, disputes and permissible purpose for consumer reporting agencies.
- FCRA disputes API — Open, track and resolve a consumer dispute against a record.
- Sex offender monitoring — Watch a person or an address and get an alert when the registry changes.
- Tenant screening API — The registry and criminal layer for property software, with FCRA workflows for rental decisions.
- Non-FCRA background check API — What a public-records lookup may be used for, and when a decision needs FCRA instead.