Offender Map API · v1.0.0

Routes, stops and corridors

Exact counts along a route, per leg and per stop, with nameless pins.

Livehttps://api.offendersearch.appSandboxhttps://sandbox.offendersearch.app

Route search: who is registered along a route

POST /v1/map/route answers who is registered along a route, exactly: within a corridor either side of a line, within a radius around each stop, or both. The answer counts each leg and each stop, and places every pin on the route. It shows registered addresses, not where anyone is.

POST /v1/map/route
curl -X POST https://api.offendersearch.app/v1/map/route \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": "bus",
    "polyline": "o|{rFnuqyN_v@oi@o}@g^",
    "stops": [
      {"id": "stop-1", "lng": -82.9988, "lat": 39.9612},
      {"id": "stop-2", "lng": -82.9920, "lat": 39.9700},
      {"id": "stop-3", "lng": -82.9870, "lat": 39.9800}
    ],
    "corridorMiles": 0.1,
    "stopRadiusFeet": 1000,
    "externalId": "route-7-AM",
    "sandbox": true
  }'

A route answer gives exact counts per leg and per stop, with pins and distances but no names. The full record behind a pin is opened, and billed, one at a time.

  • Exact, never estimated. Only the map tiles the corridor and the stop radii touch are read, and every address in them is measured. A long diagonal route reads a strip, not its bounding box.
  • One call for many routes. POST /v1/map/routes takes up to 50, all or nothing. See the batch route.
  • Test mode works here too. Send "sandbox": true; the example above has people around it.

Who it is for, with pricing worked out for a district and a walking app: the sex offender route API page.

Route path formats: GeoJSON, points and polylines

Send the line in the form your routing tool already gives you. Send at most one of path, points and polyline.

FieldFormatWhere it comes from
pathA GeoJSON LineString, or a GeoJSON Feature holding oneA drawn line, a GeoJSON file, turf.js
path or points[[lng, lat], …], longitude firstAny list of coordinates
polylineAn encoded polyline, precision 5 (the default)Google Directions: routes[0].overview_polyline.points
polyline with "polylinePrecision": 6An encoded polyline, precision 6Mapbox Directions with geometries=polyline6, OSRM and Valhalla
stops onlyTwo or more stops and no lineThe stops are joined by straight lines
  • A line may have up to 50,000 points. One with more than 2,000 is simplified to within about 5 metres first, so a turn-by-turn line from a directions service fits.
  • A polyline read at the wrong precision lands far from where it was drawn. It is refused with invalid_polyline, never answered somewhere else.
  • A route may not cross the date line or span half the globe.

Route corridor and bus stop radius

measureCountsNeeds
corridorEveryone whose registered address is within corridorMiles of any part of the line (default 0.1 miles either side).A line, or two or more stops.
stopsEveryone within stopRadiusFeet of each stop, plus any parcelAllowanceFeet.One or more stops.
bothBoth, with one total of distinct people.A line or two stops, and one or more stops.

Leave measure out and it follows what you sent: both when stopRadiusFeet is sent (stops when there is no line), otherwise corridor.

  • profile is walk (the default) or bus. It sets the limits and the default stop radius: 500 feet walking, 1,000 feet by bus.
  • parcelAllowanceFeet adds 0 to 500 feet to every stop radius. A registered address is one geocoded point; the allowance stands in for the distance from that point to the property line.
  • Stops are in route order. The line is cut into legs at the stops, and each stop is placed along the line in that order, so a loop that ends where it starts puts its last stop at the end.
  • Stop ids are yours. Answers name a stop by its id only. A label is accepted and never echoed.

Route stop fields in a request: the MapRouteStopInput object

One stop in stops[] of a route request. A bare [lng, lat] pair is accepted too, and gets the id s1, s2, … by its position.

Returned or accepted by POST /v1/map/route, POST /v1/map/routes.

FieldTypeDescription
id
stringYour id for the stop, up to 64 characters, unique within the route. Left out: s1, s2, … by position. Answers name the stop by this id.
Example "stop-2"
lng
numberLongitude of the stop, in degrees.
Example -82.992
lat
numberLatitude of the stop, in degrees.
Example 39.97
label
stringAccepted and never echoed. An answer refers to a stop by its id only, so keep your own labels on your side.

Route legs, stops and alongMiles

200 OK
{
  "shape": "route",
  "profile": "bus",
  "measure": "both",
  "detail": "full",
  "datasetVersion": "2026-10-18",
  "complete": true,
  "lengthMiles": 1.456,
  "corridorMiles": 0.1,
  "stopRadiusFeet": 1000.0,
  "parcelAllowanceFeet": 0.0,
  "units": 3,
  "tilesRead": 4,
  "total": 9,
  "corridorTotal": 8,
  "stopsTotal": 4,
  "breakdown": {"street": 9, "housed": 3, "homeless": 0, "incarcerated": 0},
  "legs": [
    {"leg": 1, "fromStop": "stop-1", "toStop": "stop-2", "lengthMiles": 0.687, "count": 5},
    {"leg": 2, "fromStop": "stop-2", "toStop": "stop-3", "lengthMiles": 0.769, "count": 3}
  ],
  "stops": [
    {"stopId": "stop-1", "alongMiles": 0.0, "count": 2, "nearestFeet": 412,
     "markerIds": ["mk_Qa8…", "mk_Rt2…"]},
  • total is distinct people with a street address in the corridor or within a stop’s reach. corridorTotal and stopsTotal split it; a person can be in both.
  • legs[] counts the corridor leg by leg. Each person is counted on the leg of their nearest point of the line, so the leg counts add up to corridorTotal.
  • stops[] gives each stop’s count, the distance to the closest address in nearestFeet, and the pins in reach in markerIds.
  • Pins are in markers (one person) and groups (several people at one address), ordered along the line. In the corridor a pin has distanceMiles, leg and alongMiles; on a route with stops it has nearestStop and stopDistanceFeet.
  • routeNotice says how the route was measured. Show it next to the counts.
  • units is what the route is billed as. See route units.

Route leg fields: the MapRouteLeg object

The line between two stops, in legs[]. With fewer than two stops the whole line is one leg. Each person in the corridor counts on the leg of their nearest point of the line, so the leg counts add up to corridorTotal.

Returned or accepted by POST /v1/map/route, POST /v1/map/routes.

FieldTypeDescription
leg
integerThe leg number, from 1.
Example 1
fromStop
string or nullThe id of the stop the leg starts at. null when the route has fewer than two stops.
toStop
string or nullThe id of the stop the leg ends at. null when the route has fewer than two stops.
lengthMiles
numberThe length of the leg, in miles. Line before the first stop and after the last belongs to the first and last leg.
count
integerPeople in the corridor whose nearest point of the line is on this leg.

Route stop fields in an answer: the MapRouteStop object

One stop, as answered, in stops[], in the order sent.

Returned or accepted by POST /v1/map/route, POST /v1/map/routes.

FieldTypeDescription
stopId
stringThe stop's id.
Example "stop-2"
alongMiles
number or nullMiles from the start of the line to the stop. null when there is no line.
count
integer or nullPeople within the stop's reach (stopRadiusFeet plus parcelAllowanceFeet). null when stops are not measured.
nearestFeet
integer or nullFeet from the stop to the closest of them. null when there is none.
markerIds
array of stringThe marker and group ids within the stop's reach. Present when stops are measured; empty with detail=count.

Route pin fields: the MapRouteMarker object

One person at one exact street address near the route, in markers[]. A position, its place on the route and an id. Never a name, on any plan.

Returned or accepted by POST /v1/map/route, POST /v1/map/routes.

FieldTypeDescription
id
stringThe marker id, mk_…. Pass it to GET /v1/map/records/{markerId} to open the record (one record open). It works for 24 hours, only for the account it was issued to.
lat
numberLatitude of the registered address, in degrees.
lng
numberLongitude of the registered address, in degrees.
precision
string
one of "street"
Always street.
addressType
string
one of "home", "work", "school", "other"
Which registered address this pin is.
distanceMiles
numberStraight-line miles from the registered address to the nearest point of the line. Present when the pin is in the corridor.
Example 0.04
leg
integerThe leg (from 1) the nearest point of the line is on. Present when the pin is in the corridor.
Example 1
alongMiles
numberMiles from the start of the line to its nearest point. Present when the pin is in the corridor.
Example 0.62
nearestStop
stringThe id of the closest stop. Present when the route has stops, whether or not the pin is within that stop's radius.
Example "stop-2"
stopDistanceFeet
integerStraight-line feet from the registered address to that stop. Present when the route has stops.
Example 412

Route group pin fields: the MapRouteGroup object

Several people registered at one exact street address near the route, as one pin in groups[]. The people behind it open with GET /v1/map/groups/{groupId} (one map request per page, no names).

Returned or accepted by POST /v1/map/route, POST /v1/map/routes.

FieldTypeDescription
groupId
stringThe group id, mg_…, for GET /v1/map/groups/{groupId}. Works for 24 hours.
lat
numberLatitude of the address, in degrees.
lng
numberLongitude of the address, in degrees.
precision
string
one of "street"
Always street.
count
integerPeople registered at this address.
distanceMiles
numberStraight-line miles from the registered address to the nearest point of the line. Present when the pin is in the corridor.
Example 0.04
leg
integerThe leg (from 1) the nearest point of the line is on. Present when the pin is in the corridor.
Example 1
alongMiles
numberMiles from the start of the line to its nearest point. Present when the pin is in the corridor.
Example 0.62
nearestStop
stringThe id of the closest stop. Present when the route has stops, whether or not the pin is within that stop's radius.
Example "stop-2"
stopDistanceFeet
integerStraight-line feet from the registered address to that stop. Present when the route has stops.
Example 412

Approximate and unplaced people near a route

Some people are mapped only to the centre of a ZIP code, a city or a county. Their distance to the route is not known, so they are never counted in total. Those whose area centre falls in the map tiles the route touches are listed under approximate, with a count and one pin per area. Draw them so they do not look like an address.

unplaced counts people in the states the route touches who have no mappable address at all, as on a map view. Say so next to the counts, so a short list is not read as the whole list.

Route approximate-area fields: the MapRouteArea object

People mapped only to the centre of a ZIP code, city or county, in approximate.areas[]. Their distance to the route is not known, so they are listed here and never counted in total.

Returned or accepted by POST /v1/map/route, POST /v1/map/routes.

FieldTypeDescription
groupId
stringThe group id, mg_…, for GET /v1/map/groups/{groupId}.
lat
numberLatitude of the area's centre.
lng
numberLongitude of the area's centre.
precision
string
one of "zip", "city", "county"
What kind of area.
place
stringThe area's name.
Example "43215"
count
integerPeople mapped to this area.

Never partial: exact route counts or a refusal

A route is answered whole, or refused with nothing charged. There is no estimated count and no cut-off list.

WhenAnswerWhat to do
More than 300 pins with detail=full422 route_too_many_people, with the exact counts in countsShow the counts, send detail=count, or split the route.
More than 50,000 registered addresses to read422 route_too_denseSplit the route, or narrow the corridor.
A read fails503 map_unavailable, with no resultRetry after Retry-After.
One route in a batch is refusedThe batch is refused, with its routeIndexFix or split that route and send the batch again.
422 Unprocessable Entity
{
  "detail": {
    "code": "route_too_many_people",
    "message": "This route has 412 places to pin, more than the 300 one response carries. …",
    "counts": {
      "total": 398,
      "corridorTotal": 371,
      "stopsTotal": 64,
      "breakdown": {"street": 398, "housed": 120, "homeless": 9, "incarcerated": 0},
      "legs": [{"leg": 1, "fromStop": "stop-1", "toStop": "stop-2", "lengthMiles": 0.61, "count": 12}],
      "stops": [{"stopId": "stop-1", "alongMiles": 0.0, "count": 3, "nearestFeet": 288}],
      "approximate": {"count": 14},
      "units": 22
    },
    "maxMarkers": 300
  }
}

Route units: route corridor pricing

A route is $2.00 per 1,000 route units: 1 unit per started mile of corridor, plus 1 per 10 stops.

Part of the routeUnitsNote
The corridor, when it is measured1 unit for each started mile of lineEvery corridor accepted, up to 0.25 miles either side, counts ×1.
The stops, when a stop radius is measured1 unit for each started 10 stopsStops sent but not measured add nothing.
Every routeAt least 1 unitA short walk with no stops is 1 unit.

Worked example. A 17.42-mile bus route with a 0.1-mile corridor and 35 stops at 1,000 feet is 18 + 4 = 22 units, $0.044. Checking 300 such routes in each of 3 terms is 19,800 units, $39.60 a year in map requests.

  • detail=count costs the same as a route with pins.
  • A batch costs the sum of its routes, and nothing unless the whole batch answers.
  • The same route asked again with If-None-Match answers 304, free. Test mode and errors are free.
  • Opening a record behind a pin is one record open, $0.035 by default. See Billing.

No names on a route answer

A route answer carries positions, distances, counts and ids. It never carries a name, on any plan, and the markerFields: named plan setting does not apply to it.

The reason is the shape. A map view is bounded by a screen, and names start only from zoom 15, a few streets across. A route can run for 60 miles, so a named answer would be a list of names along a whole county. The record behind any pin is still one call away: pass its id to GET /v1/map/records/{markerId}, one record open.

For a map with name labels, request GET /v1/map/viewport at zoom 15 or closer for the part of the route on screen. That is an ordinary map view, one map request, with named pins.

Route limits for walk and bus profiles

LimitwalkbusPast it
Longest line15 miles60 miles422 route_too_long
StopsUp to 50Up to 150422 route_too_many_stops
Corridor (corridorMiles)0.02 to 0.25 miles, default 0.10.02 to 0.25 miles, default 0.1422 invalid_corridor
Stop radius (stopRadiusFeet)100 to 2,640 feet, default 500100 to 2,640 feet, default 1,000422 invalid_radius
Parcel allowance0 to 500 feet0 to 500 feet422 invalid_radius
Points in the line50,000 sent; 2,000 after simplifying to about 5 metresThe same422 route_too_many_points
Pins in a detail=full answer300, counting markers, groups and approximate areasThe same422 route_too_many_people, with the exact counts
Registered addresses one route reads50,000The same422 route_too_dense
Routes in one batch5050422 route_batch_too_large

GET /v1/map/status returns these under routes.profiles. An end user may check 20 different routes a day by default; see Daily limits.

Route profile limits: the MapRouteProfile object

The limits of one route profile, as GET /v1/map/status reports them under routes.profiles.

Returned or accepted by GET /v1/map/status.

FieldTypeDescription
maxMiles
numberThe longest line, in miles.
Example 60
maxStops
integerThe most stops.
Example 150
corridorMiles
array of numberThe narrowest and widest corridor, in miles either side of the line.
Example [0.02,0.25]
stopRadiusFeet
array of numberThe smallest and largest stop radius, in feet.
Example [100,2640]

Route API samples: curl, Node and Python

curl -X POST https://api.offendersearch.app/v1/map/route \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profile": "bus",
    "polyline": "o|{rFnuqyN_v@oi@o}@g^",
    "stops": [
      {"id": "stop-1", "lng": -82.9988, "lat": 39.9612},
      {"id": "stop-2", "lng": -82.9920, "lat": 39.9700},
      {"id": "stop-3", "lng": -82.9870, "lat": 39.9800}
    ],
    "corridorMiles": 0.1,
    "stopRadiusFeet": 1000,
    "externalId": "route-7-AM",
    "sandbox": true
  }'

Sex offender map API: POST /v1/map/route request and response fields

POST/v1/map/routeAuth: X-API-Key

Exact counts and nameless pins along one route: within a corridor either side of a line, within a radius of each stop, or both, per leg and per stop. Billed as route units, each one map request; detail=count costs the same; a 304, test mode and errors are free. Never partial: a route is answered whole or refused with nothing charged.

Request

Headers
FieldTypeDescription
X-API-Key
required
stringA secret API key (from your server), or a map browser key os_map_pk_… sent from a site on that key's list. A dashboard session can send Authorization: Bearer <token> instead.
X-End-User
optional
stringAny stable id you use for the person behind this request, such as a hashed user id. Required with a map browser key (422 end_user_required); optional with a secret key. Pins are counted against that user's daily area budget (2,000 distinct areas), and each different route against their daily route limit (20 by default).
If-None-Match
optional
stringThe ETag of an earlier answer for the same route. Unchanged, it answers 304 with no body and is not billed.
Body · application/json
FieldTypeDescription
path
optional
object or arrayThe line: a GeoJSON LineString, a GeoJSON Feature holding one, or a list of [lng, lat] points. Up to 50,000 points; a line with more than 2,000 is simplified to within about 5 metres first.
Example {"type":"LineString","coordinates":[[-83.0006,39.9601],[-82.9951,39.9608]]}
points
optional
array of arrayThe line as [[lng, lat], …], longitude first: the same list POST /v1/map/area takes.
Example [[-83.0006,39.9601],[-82.9951,39.9608]]
polyline
optional
stringThe line as an encoded polyline, exactly as a directions service returns it: Google Directions overview_polyline.points (precision 5), or a Mapbox Directions geometries=polyline6 geometry, OSRM or Valhalla (precision 6).
Example "o|{rFnuqyN_v@oi@o}@g^"
polylinePrecision
optional · default 5
integer
one of 5, 6
The precision of polyline: 5 for Google, 6 for Mapbox, OSRM and Valhalla. Read at the wrong precision a line lands far away and is refused with invalid_polyline.
stops
optional
array of MapRouteStopInputThe stops, in route order: {id, lng, lat} objects, or [lng, lat] pairs. Walk: up to 50. Bus: up to 150. The line is cut into legs at the stops.
profile
optional · default "walk"
string
one of "walk", "bus"
Sets the limits and the default stop radius. walk: up to 15 miles, 50 stops, 500 feet. bus: up to 60 miles, 150 stops, 1,000 feet.
measure
optional
string
one of "corridor", "stops", "both"
What is counted: the corridor along the line, a radius around each stop, or both. Left out: both when stopRadiusFeet is sent (stops when there is no line), otherwise corridor.
corridorMiles
optional · default 0.1
numberThe distance either side of the line, 0.02 to 0.25 miles.
stopRadiusFeet
optional
numberThe radius around each stop, 100 to 2,640 feet. Left out: 500 for walk, 1,000 for bus.
Example 1000
parcelAllowanceFeet
optional · default 0
number0 to 500 feet added to the stop radius. A registered address is a geocoded point; an allowance stands in for the distance from that point to the property line.
detail
optional · default "full"
string
one of "full", "count"
full for counts and nameless pins, count for counts only (no pins and no ids). The same price.
format
optional · default "json"
string
one of "json", "geojson"
json for the MapRouteResponse shape, geojson for a MapGeoJson FeatureCollection. A batch answers in json only.
housing
optional
string or array of stringKeep only people with these housing statuses: a comma-separated string or a list of housed, homeless, incarcerated.
Example ["homeless"]
precision
optional
string
one of "street", "any"
street leaves out the people counted under approximate. any, or leaving it out, keeps everyone.
sandbox
optional · default false
booleantrue answers in test mode: made-up people, never billed.
externalId
optional
stringYour own id for the route, up to 100 characters, echoed in the answer.
Example "route-7-AM"

Response

200 · application/json

The answer for this route, with Cache-Control: private, max-age=300 and an ETag. With format: "geojson" the body is a MapGeoJson FeatureCollection.

FieldTypeDescription
routeIndex
integerIn a POST /v1/map/routes answer only: the route's position in the request, from 0.
shape
string
one of "route"
Always route.
profile
string
one of "walk", "bus"
The profile the route was answered with.
measure
string
one of "corridor", "stops", "both"
What was counted.
detail
string
one of "full", "count"
count answers have empty markers, groups and approximate.areas.
datasetVersion
string (date)The version of the data that answered.
Example "2026-10-18"
complete
booleanAlways true. A route is answered whole or refused.
lengthMiles
numberThe length of the line, in miles. 0 with no line.
Example 17.42
corridorMiles
number or nullThe corridor answered, in miles either side of the line. null when the corridor is not measured.
Example 0.1
stopRadiusFeet
number or nullThe stop radius answered, in feet. null when stops are not measured.
Example 1000
parcelAllowanceFeet
number or nullThe allowance added to the stop radius, in feet. null when stops are not measured.
units
integerRoute units: the number of map requests this route is billed as. See Route units.
Example 22
tilesRead
integerThe zoom-14 map tiles the corridor and the stop radii touch.
total
integerDistinct people with a street address in the corridor or within a stop's reach. People under approximate are never in it.
corridorTotal
integer or nullPeople in the corridor. The leg counts add up to it. null when the corridor is not measured.
stopsTotal
integer or nullDistinct people within reach of any stop. null when stops are not measured.
breakdown
MapBreakdownOf total: street addresses, and housing status where the registry states it.
legs
array of MapRouteLegOne entry per leg. Empty when the corridor is not measured.
stops
array of MapRouteStopOne entry per stop, in the order sent.
markers
array of MapRouteMarkerOne nameless pin per person at a street address, ordered along the line. Empty with detail=count.
groups
array of MapRouteGroupOne pin per address where several people are registered. Empty with detail=count.
approximate
objectPeople mapped only to the centre of a ZIP code, city or county inside the map tiles the route touches. Their distance to the route is not known, so they are never in total.
approximate.count
integerPeople mapped only to an area.
approximate.areas
array of MapRouteAreaOne pin per area. Empty with detail=count.
unplaced
MapUnplacedPeople in the states the route touches who have no mappable address.
externalId
stringYour externalId, when you sent one.
sandbox
booleanPresent and true on a test-mode answer.
notice
stringA sentence to show with the answer.
Example "A pin shows a registered address, not a person's current location."
routeNotice
stringHow a route is measured, as a sentence to show next to the counts: distances are straight-line from each geocoded registered address, and a count shows registered addresses, not where anyone is.
304 · no body

The same route was asked within the ETag's life. No body, not billed.

Errors
StatusCodeWhen
401—The key is missing or invalid. The body is the plain detail string.
403map_not_enabledThe Map API is off for this account (map_paused when we paused it, origin_not_allowed for a browser key used from a site not on its list). Send sandbox: true to build first.
422invalid_pathThe route could not be read: invalid_path, invalid_polyline, invalid_stops, invalid_profile, invalid_measure, invalid_corridor, invalid_radius, invalid_detail, invalid_format, invalid_filter. Nothing was charged.
422route_too_longLonger than the profile allows (route_too_many_stops for too many stops, route_too_many_points for a line still over 2,000 points after simplifying). Split the route.
422route_too_many_peopleMore than 300 pins with detail=full. counts holds the exact counts; nothing was charged. Send detail=count, or split the route.
422route_too_denseThe route passes through more registered addresses than one call counts exactly. Split it, or narrow the corridor. Nothing was charged.
422end_user_requiredA map browser key sent no X-End-User header.
429map_end_user_route_limitThis end user has checked their daily number of different routes. A route already checked today can be checked again. map_sweep_limit and map_end_user_limit when a daily area budget is used by pins (detail=count is never refused by them); map_busy when too many map requests arrive at once.
503map_unavailableThe route could not be read just now: no result and nothing charged. Retry after Retry-After (map_index_not_ready, map_not_configured likewise).

Route GeoJSON output

With "format": "geojson" a single route answers as a FeatureCollection of points, content type application/geo+json: one feature per marker (kind: "marker"), group (kind: "group") and approximate area (kind: "area"). Everything else, the counts, legs and stops included, is under meta. A batch answers in JSON only.

POST /v1/map/route
curl -X POST https://api.offendersearch.app/v1/map/route \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "path": {
      "type": "LineString",
      "coordinates": [[-82.9988, 39.9612], [-82.9920, 39.9700], [-82.9870, 39.9800]]
    },
    "format": "geojson",
    "sandbox": true
  }'
format: geojson
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {"type": "Point", "coordinates": [-82.99863, 39.96133]},
      "properties": {
        "kind": "marker",
        "id": "mk_Qa8…",
        "precision": "street",
        "addressType": "home",
        "distanceMiles": 0.002,
        "leg": 1,
        "alongMiles": 0.078
      }
    },
    {
      "type": "Feature",
      "geometry": {"type": "Point", "coordinates": [-83.0114, 39.969]},
      "properties": {
        "kind": "area",
        "groupId": "mg_X1G…",
        "precision": "zip",
        "place": "43215",
        "count": 5
      }
    }
  ],
  "meta": {
    "shape": "route", "measure": "corridor", "units": 2, "total": 8,
    "legs": [{"leg": 1, "fromStop": null, "toStop": null, "lengthMiles": 1.456, "count": 8}],
    "approximate": {"count": 5, "areas": ["…"]},
    "notice": "A pin shows a registered address, not a person's current location."
  }
}

Batch route search: up to 50 routes

POST /v1/map/routes takes up to 50 routes, each shaped like a POST /v1/map/route body, and answers them in the order sent, each with its routeIndex.

  • All or nothing. One refused route refuses the batch, with routeIndex naming it, and nothing is billed.
  • One response for every route. A person near three routes is one record open when opened from this answer, and is counted once in uniquePeople.
  • Billing is the sum of the routes’ units, in units.
curl -X POST https://api.offendersearch.app/v1/map/routes \
  -H "X-API-Key: $OFFENDERSEARCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "routes": [
      {"externalId": "route-7-AM", "profile": "bus", "polyline": "o|{rFnuqyN_v@oi@o}@g^",
       "stops": [[-82.9988, 39.9612], [-82.9870, 39.9800]], "stopRadiusFeet": 1000},
      {"externalId": "route-7-PM", "profile": "bus",
       "points": [[-82.9870, 39.9800], [-82.9988, 39.9612]], "detail": "count"}
    ],
    "sandbox": true
  }'
200 OK
{
  "shape": "routes",
  "complete": true,
  "datasetVersion": "2026-10-18",
  "units": 4,
  "uniquePeople": 9,
  "routes": [
    {"routeIndex": 0, "shape": "route", "externalId": "route-7-AM", "total": 9, "…": "…"},
    {"routeIndex": 1, "shape": "route", "externalId": "route-7-PM", "total": 8, "…": "…"}
  ],
  "notice": "A pin shows a registered address, not a person's current location.",
  "sandbox": true
}

Sex offender map API: POST /v1/map/routes request and response fields

POST/v1/map/routesAuth: X-API-Key

Up to 50 routes in one call, answered all or nothing: any refused route refuses the batch, with its routeIndex, and nothing is billed. Billed as the sum of the routes' units. Every marker shares one response, so a person near several routes is one record open.

Request

Headers
FieldTypeDescription
X-API-Key
required
stringA secret API key (from your server), or a map browser key os_map_pk_… sent from a site on that key's list. A dashboard session can send Authorization: Bearer <token> instead.
X-End-User
optional
stringAny stable id you use for the person behind this request, such as a hashed user id. Required with a map browser key (422 end_user_required); optional with a secret key. Pins are counted against that user's daily area budget (2,000 distinct areas), and each different route against their daily route limit (20 by default).
Body · application/json
FieldTypeDescription
routes
required
array of MapRouteRequest1 to 50 routes, each a MapRouteRequest. format must be json or left out.
sandbox
optional · default false
booleantrue answers every route in test mode.

Response

200 · application/json

Every route's answer, in the order sent.

FieldTypeDescription
shape
string
one of "routes"
Always routes.
complete
booleanAlways true. A batch is answered whole or refused.
datasetVersion
string (date)The version of the data that answered.
units
integerThe sum of the routes' units: the map requests this call is billed as.
uniquePeople
integerDistinct people across every route. A person near three routes counts once.
routes
array of MapRouteResponseOne answer per route, each with its routeIndex.
sandbox
booleanPresent and true on a test-mode answer.
notice
stringA sentence to show with the answer.
Errors
StatusCodeWhen
401—The key is missing or invalid. The body is the plain detail string.
403map_not_enabledAs on POST /v1/map/route.
422route_batch_too_largeMore than 50 routes (invalid_route for none, invalid_format for a route asking for GeoJSON). Any POST /v1/map/route refusal, with routeIndex naming the route. Nothing was charged.
429map_end_user_route_limitAs on POST /v1/map/route.
503map_unavailableA route could not be read just now: no result and nothing charged. Retry after Retry-After.

Route caching and end users

  • A single route answer carries Cache-Control: private, max-age=300 and an ETag. Send it back in If-None-Match: the same route answers 304, with no body, free.
  • Marker and group ids work for 24 hours, for your account only.
  • A map browser key must send X-End-User on both route routes. See Platform integration.
  • Pins from a route count toward the daily area limit like pins from a view (detail=count never does), and each end user may check 20 different routes a day by default. See Limits & errors.

Route search errors

CodeStatusCauseWhat to do
invalid_route422The route is not a JSON object, or a batch has no routes.Send a route object; send 1 to 50 in a batch.
invalid_path422More than one of path, points and polyline; a point that is not [lng, lat]; a line of one point; a corridor with no line; or a route that crosses the date line.Send one line, longitude first.
invalid_polyline422The polyline does not decode, or decodes off the globe.Check polylinePrecision: 5 for Google, 6 for Mapbox, OSRM and Valhalla.
invalid_profile422profile is not walk or bus.Send one of the two.
invalid_measure422measure is not corridor, stops or both.Send one of the three, or leave it out.
invalid_corridor422corridorMiles is outside 0.02 to 0.25.Send a corridor inside the range.
invalid_radius422stopRadiusFeet or parcelAllowanceFeet is outside its range.See the limits above.
invalid_stops422A stop is not {id, lng, lat} or [lng, lat], an id is used twice, or stops are measured with no stops.Fix the stop, or measure the corridor only.
invalid_detail, invalid_format, invalid_filter422As on the viewport route. A batch answers in json only.Send an accepted value.
route_too_long422Longer than the profile allows, or touching more map area than one call reads.Split the route into shorter routes.
route_too_many_points422More than 50,000 points, or more than 2,000 after simplifying.Split the route.
route_too_many_stops422More stops than the profile allows.Split the route, or use bus.
route_batch_too_large422More than 50 routes in a batch.Send at most 50 per call.
route_too_dense422The route passes through more registered addresses than one call counts exactly.Split the route, or narrow the corridor.
route_too_many_people422More than 300 pins with detail=full. counts holds the exact counts.Show the counts, send detail=count, or split the route.
end_user_required422A map browser key sent no X-End-User.Send any stable id for your user, such as a hashed user id.
map_end_user_route_limit429This end user has checked 20 different routes today (the default).A route already checked today can be checked again. Other users are not affected. It resets at 00:00 UTC.
map_unavailable503The route could not be read just now. No result, nothing charged.Retry after the Retry-After header.

Access errors (401, 403) and service errors are as on every map route. See Limits & errors.

Product overview