Docs API reference
API reference
The public HTTP APIs: vector tiles and live updates under /t, geofencing under /v1,
and the webhooks we send you. For setup and concepts, start with the guide.
Overview
Base URLs
https://tiles.mapsocket.com/t- Tiles, TileJSON and live update streams. Pro and Enterprise can serve tiles from their own domain.
https://api.mapsocket.com/v1- Geofencing: tracking, geofences, trips, places, beacons, events, users and fraud checks.
Requests and responses are JSON unless noted. IDs are UUIDs, timestamps are ISO-8601 strings, and coordinates are
WGS84. GeoJSON positions are [lng, lat]; the coordinates and near query
parameters are lat,lng. Both APIs answer CORS requests from any origin; publishable keys add their own
origin check.
Authentication
Every request needs an API key from the project's API keys page in the dashboard. Keys belong to one project.
pk_live_…publishable- Safe to ship in browser and mobile code. Works for tiles, track, context, geofence and place search, the check endpoints, and creating and updating trips. Each endpoint below is marked with the keys it accepts.
sk_live_…secret- Servers only. Works everywhere, and is the only key for managing geofences, beacons and places, reading trips, events and users, and blocking users.
Send the key as an Authorization header, with or without Bearer, or as a
?key= query parameter. The header wins when both are present.
# Three equivalent requests. The header is best for servers and apps.
curl "https://api.mapsocket.com/v1/context?coordinates=40.7411,-73.9897" -H "Authorization: pk_live_…"
curl "https://api.mapsocket.com/v1/context?coordinates=40.7411,-73.9897" -H "Authorization: Bearer pk_live_…"
curl "https://api.mapsocket.com/v1/context?coordinates=40.7411,-73.9897&key=pk_live_…" Allowed origins. A publishable key can be limited to a list of origins, either exact hosts
(maps.example.com) or wildcards (*.example.com, which also matches
example.com). Requests from a browser on any other origin get 403 origin_not_allowed.
An empty list allows every origin. The check uses the Origin header, so requests without one, such as
from a server or a native app, aren't affected. Secret keys have no origin list.
Errors
Every error has a matching HTTP status and the same body. Branch on code; the
message is written for people and may change.
{
"error": {
"code": "secret_key_required",
"message": "This endpoint needs a secret key (sk_live_…). Never ship secret keys to browsers or apps."
}
} Status codes
| Status | Error codes | When |
|---|---|---|
| 200 | — | Success. |
| 201 | — | Something was created: a geofence, trip, beacon or place (POST, or PUT for a new tag and external id). |
| 204 | — | Success with no body: a delete, or an empty tile. |
| 304 | — | The tile matches the ETag you sent in If-None-Match. |
| 400 | bad_requestinvalid_inputaddress_not_foundbad_intervalbad_tile | Malformed JSON, a bad coordinate string, a field that fails validation (the message names it), an update to an ended trip, an address the geocoder can't find, or a contour interval or tile outside its range. |
| 401 | invalid_key | No key, a malformed key, or a revoked key. |
| 402 | plan_limit | The project is at its plan's geofence limit, or a new user would exceed the monthly tracked-user limit. |
| 403 | secret_key_requiredorigin_not_allowed | A publishable key on a secret-only endpoint, or a publishable key used from an origin it doesn't allow. |
| 404 | not_found | No such endpoint, tile source, geofence, trip, event, beacon, place or user in this key's project. |
| 429 | rate_limitedmonthly_quota_exceeded | Too many requests for this key (see Retry-After), or the Sandbox's monthly tile requests are used up. |
| 500 | internal | Something failed on MapSocket's side. Retry with backoff. |
| 501 | geocoder_not_configuredrouting_not_configured | A geofence with an address, or an isochrone geofence, on a server without a geocoder or routing engine. Send coordinates, or use a circle or polygon. |
| 502 | worker_unavailablegeocoder_unavailablerouting_unavailableelevation_source_unavailable | The tile worker, geocoder, routing engine or elevation source didn't respond. Retry shortly. |
| 503 | source_pausedsource_unavailable | The tile source is paused, or isn't live yet (still building, or failed). |
Rate limits and quotas
Each key has a token bucket: it refills at the plan's rate per second and holds up to the burst size. Every
/t and /v1 request takes one token. When the bucket is empty you get
429 rate_limited with a Retry-After header in seconds.
| Plan | Requests / second | Burst | Tile requests / month | Geofences | Tracked users / month |
|---|---|---|---|---|---|
| Sandbox | 20 | 60 | 50,000 | 100 | 100 |
| Starter | 100 | 300 | 1,000,000 | 5,000 | 5,000 |
| Pro | 500 | 1,500 | 10,000,000 | 100,000 | 50,000 |
| Enterprise | 2,000 | 6,000 | Custom | Custom | Custom |
- Tile requests count every tile response, including
204and304. TileJSON and event streams don't count. On the Sandbox, tiles stop with429 monthly_quota_exceededafter 50,000 requests in a calendar month (UTC), until you upgrade or the month ends. Paid plans keep serving past their quota; the overage shows in usage. - Geofences are capped per project. Creating one past the cap returns
402 plan_limit; replacing an existing one doesn't count. - Tracked users are the distinct
userIds seen by/v1/trackthis month. A new user past the cap gets402 plan_limit; users already seen this month keep working.
Tiles
Each tile source is served at https://tiles.mapsocket.com/t/:project/:source. The examples use the project
acme and the source parcels. Use a publishable key; the source must belong to the key's
project.
GET /t/:project/:source.json
Publishable or secret key
TileJSON 3.0.0 for the source: zoom range, vector layers and their fields, and a tiles template that
already carries your key. Point your map's source url here. Cached for 60 seconds.
| Name | Type | Description |
|---|---|---|
project required | path · string | Your project's slug, shown on the source page in the dashboard. |
source required | path · string | The source name, e.g. parcels. |
key required | query · string | A publishable key for the same project. An Authorization header also works, but map clients and EventSource can't set headers. |
Pass the key as ?key= here: the tiles template copies the query key, so a key sent only
in a header leaves the tile URLs without one.
curl "https://tiles.mapsocket.com/t/acme/parcels.json?key=pk_live_…" {
"tilejson": "3.0.0",
"name": "parcels",
"minzoom": 10,
"maxzoom": 15,
"tiles": [
"https://tiles.mapsocket.com/t/acme/parcels/{z}/{x}/{y}.pbf?key=pk_live_…"
],
"vector_layers": [
{
"id": "parcels",
"fields": {
"owner": "",
"zoning": ""
}
},
{
"id": "roads",
"fields": {
"class": "",
"name": ""
}
}
]
} GET /t/:project/:source/:z/:x/:y.pbf
Publishable or secret key
One Mapbox Vector Tile (application/vnd.mapbox-vector-tile), gzip-compressed with
Content-Encoding: gzip. Map libraries handle this for you. Responses carry an ETag
and Cache-Control.
| Name | Type | Description |
|---|---|---|
project required | path · string | Your project's slug, shown on the source page in the dashboard. |
source required | path · string | The source name, e.g. parcels. |
zxy required | path · integer | Tile coordinates in the XYZ (slippy map) scheme, ending in .pbf. |
key required | query · string | A publishable key for the same project. An Authorization header also works, but map clients and EventSource can't set headers. |
If-None-Match optional | header · string | The ETag from an earlier response. Unchanged tiles answer 304 with no body. |
200- The tile.
204- No data in this tile. Normal for empty areas; map clients don't log it as an error.
304- Your cached copy is current.
503source_pausedorsource_unavailable(still building, or the last build failed).429rate_limited, ormonthly_quota_exceededon the Sandbox.
curl -i "https://tiles.mapsocket.com/t/acme/parcels/14/4823/6160.pbf?key=pk_live_…" \
-H 'If-None-Match: "…"' HTTP/1.1 304 Not Modified
etag: "…"
content-type: application/vnd.mapbox-vector-tile GET /t/:project/:source/events
Publishable or secret key
A server-sent events stream
that tells you when the source's tiles change, so the map can refetch them. Open it with
EventSource and the key in ?key=. The connection stays open; browsers reconnect on
their own if it drops.
| Name | Type | Description |
|---|---|---|
project required | path · string | Your project's slug, shown on the source page in the dashboard. |
source required | path · string | The source name, e.g. parcels. |
key required | query · string | A publishable key for the same project. An Authorization header also works, but map clients and EventSource can't set headers. |
| Event | Sent when | Data fields |
|---|---|---|
update_complete | Rows changed in a live-mode source and the affected tiles were rewritten. | source name, tiles_updated count, affected_zooms (array of zoom
levels), max_zoom, layers_affected (array of layer names)
|
generate_complete | A full build finished: a deploy, a rebuild, or a scheduled snapshot refresh. | source name, duration_ms |
event: update_complete
data: {"source":"parcels","tiles_updated":42,"affected_zooms":[12,13,14,15],"max_zoom":15,"layers_affected":["parcels"]}
event: generate_complete
data: {"source":"parcels","duration_ms":8120} MapLibre example
Add the source by its TileJSON URL, then listen for changes. Tiles are cached by the browser and any CDN, so a
refresh swaps in a tile URL with a throwaway _t parameter.
import maplibregl from "maplibre-gl";
const key = "pk_live_…";
const base = "https://tiles.mapsocket.com/t/acme/parcels";
const map = new maplibregl.Map({ container: "map", style: "/style.json" });
map.on("load", () => {
map.addSource("parcels", { type: "vector", url: `${base}.json?key=${key}` });
map.addLayer({
id: "parcels",
type: "fill",
source: "parcels",
"source-layer": "parcels",
paint: { "fill-color": "#2D6E9E", "fill-opacity": 0.3 },
});
// Live updates: refetch tiles when the source changes.
const tiles = `${base}/{z}/{x}/{y}.pbf?key=${key}`;
const refresh = () => map.getSource("parcels").setTiles([`${tiles}&_t=${Date.now()}`]);
const events = new EventSource(`${base}/events?key=${key}`);
events.addEventListener("update_complete", (e) => {
const { affected_zooms } = JSON.parse(e.data);
// Skip refreshes that don't touch the zoom the user is looking at.
if (affected_zooms.includes(Math.floor(map.getZoom()))) refresh();
});
events.addEventListener("generate_complete", refresh);
}); Terrain
Elevation contour lines for anywhere on Earth, as vector tiles you add under or over your own layers, and elevation at any point. They aren't tied to a tile source: any key of the project works, and contour tiles count toward the monthly tile requests like any other tile. Elevation data is Terrain Tiles (SRTM, GMTED, ETOPO1 and others), roughly 20–30 m resolution; show its attribution, which the TileJSON includes.
GET /t/terrain/contours.json
Publishable or secret key
TileJSON for the contour layer. Point your map's vector source at this URL.
| Name | Type | Description |
|---|---|---|
key required | string | A publishable or secret key, as ?key= or the Authorization header. |
units optional | string | m (default) or ft. Lines are drawn at round values in this unit. |
interval optional | number | Fixed spacing between lines, 1–5,000 in units. Every fifth line is an index line. Default: spacing chosen by zoom (table below). |
curl "https://tiles.mapsocket.com/t/terrain/contours.json?key=pk_live_…&units=m" {
"tilejson": "3.0.0",
"name": "Contours",
"tiles": [
"https://tiles.mapsocket.com/t/terrain/contours/{z}/{x}/{y}.pbf?key=pk_live_…"
],
"minzoom": 8,
"maxzoom": 16,
"attribution": "Terrain Tiles: SRTM, GMTED, ETOPO1 and others",
"vector_layers": [
{
"id": "contours",
"minzoom": 8,
"maxzoom": 16,
"fields": {
"ele": "Number",
"level": "Number"
}
}
]
} GET /t/terrain/contours/:z/:x/:y.pbf
Publishable or secret key
A Mapbox Vector Tile with one layer, contours, of line features. Tiles are generated on first
request and cached for a day (Cache-Control: public, max-age=86400). Zooms below 8 and areas with
no relief (open sea) answer 204. Same query parameters as the TileJSON.
| Name | Type | Description |
|---|---|---|
ele optional | number | Elevation of the line, in the requested units. |
level optional | number | 1 for index (major) lines, 0 for the lines between them. |
| Name | Type | Description |
|---|---|---|
z8–9 optional | 200 / 1,000 | Meters between lines / between index lines. Feet: 500 / 2,500. |
z10 optional | 100 / 500 | Feet: 250 / 1,000. |
z11 optional | 50 / 250 | Feet: 200 / 1,000. |
z12 optional | 50 / 200 | Feet: 100 / 500. |
z13 optional | 20 / 100 | Feet: 50 / 250. |
z14 optional | 10 / 50 | Feet: 40 / 200. |
z15–16 optional | 5 / 25 | Feet: 20 / 100. Beyond z16 maps overzoom the z16 tile. |
GET /v1/elevation
Publishable or secret key
Elevation at up to 100 points, interpolated from the elevation grid. elevation is
null where there's no data. Summits read somewhat low, because each grid cell averages the
terrain around it.
| Name | Type | Description |
|---|---|---|
locations required | string | 1–100 latitude,longitude pairs separated by |. |
units optional | string | m (default) or ft. |
curl "https://api.mapsocket.com/v1/elevation?locations=45.8326,6.8652|38.7139,-9.1334&units=m" \
-H "Authorization: pk_live_…" {
"units": "m",
"elevations": [
{
"latitude": 45.8326,
"longitude": 6.8652,
"elevation": 4780.1
},
{
"latitude": 38.7139,
"longitude": -9.1334,
"elevation": 89.2
}
]
} Styling contours
Thin minor lines, heavier index lines, and labels along the index lines. Labels need a glyphs URL
in your style.
map.addSource("contours", {
type: "vector",
url: "https://tiles.mapsocket.com/t/terrain/contours.json?key=pk_live_…",
});
map.addLayer({
id: "contour-lines",
type: "line",
source: "contours",
"source-layer": "contours",
paint: {
"line-color": "#8C6A3A",
"line-opacity": 0.6,
"line-width": ["match", ["get", "level"], 1, 1.2, 0.5],
},
});
map.addLayer({
id: "contour-labels",
type: "symbol",
source: "contours",
"source-layer": "contours",
filter: ["==", ["get", "level"], 1],
layout: {
"symbol-placement": "line",
"text-field": ["concat", ["to-string", ["get", "ele"]], " m"],
"text-font": ["Noto Sans Regular"], // any font your style's glyphs URL serves
"text-size": 10,
},
paint: { "text-color": "#8C6A3A", "text-halo-color": "#ffffff", "text-halo-width": 1.5 },
}); Georeferenced maps
Scanned maps, site plans and aerial photos you georeferenced in the dashboard: upload the image, match at least three points on it to the map, pick a transform (affine, 2nd-order polynomial or thin-plate spline), optionally crop the margin, and publish. Publishing warps the image to Web Mercator and serves it as raster tiles from its native zoom down eight levels; the dashboard also offers the warped Cloud-Optimized GeoTIFF for download. Any key of the project works, and tiles count toward the monthly tile requests.
GET /t/georef/:id.json
Publishable or secret key
404 not_published until the map has been published once. Republishing replaces the tiles in place.
curl "https://tiles.mapsocket.com/t/georef/3b1f0a52-6c1e-4f3e-9a7d-2f4c8e1b9d60.json?key=pk_live_…" {
"tilejson": "3.0.0",
"name": "1911 harbour survey",
"scheme": "xyz",
"tiles": [
"https://tiles.mapsocket.com/t/georef/3b1f0a52-6c1e-4f3e-9a7d-2f4c8e1b9d60/{z}/{x}/{y}.png?key=pk_live_…"
],
"bounds": [-9.1516, 38.6989, -9.1281, 38.7173],
"minzoom": 9,
"maxzoom": 17,
"tileSize": 256
} GET /t/georef/:id/:z/:x/:y.png
Publishable or secret key
A 256 px PNG with transparency outside the image (or its crop). Tiles outside the image's area or zoom range answer
204. Cache-Control: public, max-age=3600.
map.addSource("harbour-1911", {
type: "raster",
url: "https://tiles.mapsocket.com/t/georef/3b1f0a52-6c1e-4f3e-9a7d-2f4c8e1b9d60.json?key=pk_live_…",
tileSize: 256,
});
map.addLayer({ id: "harbour-1911", type: "raster", source: "harbour-1911", paint: { "raster-opacity": 0.8 } }); Tracking
Base URL https://api.mapsocket.com/v1. Send each user's location to /v1/track. MapSocket keeps their
current geofences, place, country and state, beacons, active trip and fraud state, and emits an
event whenever one of those changes. Events come back in the response and go to your
webhooks.
How a track is evaluated
- Accuracy. A fix less accurate than the project's maximum (default 1,000 m) updates the user's location but changes nothing else, so it fires no events. One bad GPS reading can't produce a false exit.
- Geofences. Circles match by geodesic distance from the center; polygons and isochrones match
points inside or on the boundary. Optional project settings buffer entries (enter when within
accuracyof the edge) and exits (leave only when more thanaccuracyoutside). Geofences limited touserIdsoripranges only exist for those users and IPs. - Stop detection. When a geofence (or the project) uses it, entry waits until a track has
stopped: trueorforeground: true. Exits don't wait. - Operating hours. With the project's restrict to operating hours setting, geofences
outside their hours still track who's inside but send no entry, exit or dwell events. Every geofence event
carries
currentlyOperatingeither way. - Places need
stopped: trueto enter; regions, beacons and trips update on every accurate fix. See their sections below. - Fraud checks run on every fix and are stored on the user. Events still fire for failing fixes unless the project suppresses them.
- Dwell and exit are evaluated when a track arrives: nothing fires between a user's tracks. Idle trips expire on a one-minute schedule.
Confidence
Geofence entries and dwells carry a numeric confidence from the fix's position and
accuracy. Place entries use 3 when accuracy is within the visit radius, else 2.
3- High. The fix is inside and the whole accuracy circle is inside too (distance to the edge ≥
accuracy). 2- Medium. The fix is inside, but the accuracy circle crosses the edge.
1- Low. The fix is outside, within
accuracyof the edge. Only possible with buffered entries turned on. null- Exits, dwells, regions, beacons and trips: confidence doesn't apply.
POST /v1/track
Publishable or secret key
Record a user's location. Returns the updated user, the events this location produced (often none), the
geofences the user is in, and the current place, country and state. The response below is trimmed: with an
active trip, every track also returns a user.updated_trip event.
| Name | Type | Description |
|---|---|---|
userId required | string | Your id for the person or vehicle, 1–128 characters. State (geofences, place, regions, beacons, trip, fraud) is kept per user. |
latitude required | number | WGS84 latitude, −90 to 90. |
longitude required | number | WGS84 longitude, −180 to 180. |
accuracy optional | number | Horizontal accuracy in meters. Sets confidence. Fixes less accurate than the project's maximum (default 1,000 m) update the location but fire no events. |
stopped optional | boolean | The user isn't moving. Needed for place visits, and for entries into geofences with stop detection. |
foreground optional | boolean | The app is in the foreground. Counts as stopped for stop detection (not for places). |
deviceId optional | string | Up to 128 characters. Kept from earlier calls when omitted. A jump between two devices is reported as fraud_jumped_multiple_devices. |
deviceType optional | "iOS" | "Android" | "Web" | "Other" | Stored on the user. Kept when omitted. |
deviceOSdeviceMakedeviceModel optional | string | Up to 64 characters each. Stored on the user. Kept when omitted. |
description optional | string | Up to 256 characters. Stored on the user. |
metadata optional | object | Up to 32 keys. Replaces the user's metadata when present. |
mocked optional | boolean | Your app detected a mock location provider. Fails the fraud check with fraud_mocked_from_mock_provider. |
beacons optional | array | Beacons in range, up to 50: { type: "ibeacon", uuid, major, minor } or { type: "eddystone", uid, instance }. Send [] when none are in range; omit the field to leave beacon state unchanged. |
recordedAt optional | string | When the fix was taken, ISO-8601 with an offset. updatedAt is accepted as an alias. Defaults to the time we receive it. Dwell, exit and jump checks use this clock. |
curl -X POST "https://api.mapsocket.com/v1/track" \
-H "Authorization: pk_live_…" \
-H "Content-Type: application/json" \
-d '{
"userId": "driver-118",
"deviceId": "ios-7F3A",
"deviceType": "iOS",
"latitude": 40.7413,
"longitude": -73.9895,
"accuracy": 12,
"stopped": true,
"beacons": [{ "type": "ibeacon", "uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e", "major": 100, "minor": 1 }]
}' {
"user": {
"id": "dae33c61-be99-4f50-9680-2d192984da7c",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"description": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"stopped": true,
"foreground": null,
"deviceType": "iOS",
"deviceOS": "18.1",
"deviceMake": "Apple",
"deviceModel": "iPhone16,1",
"geofenceIds": ["6863c6f2-cce7-4a8c-9acc-31641cd0678a"],
"geofences": [
{
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
}
}
],
"place": {
"id": "1c8cdf41-760a-4cfe-aed0-8e0699bf4e4f",
"name": "Corner Café",
"chain": {
"slug": "corner-cafe",
"name": "Corner Café"
},
"categories": ["cafe", "food-beverage"]
},
"country": {
"type": "country",
"code": "US",
"name": "United States of America",
"flag": "🇺🇸",
"allowed": true
},
"state": {
"type": "state",
"code": "NY",
"name": "New York"
},
"beacons": [
{
"id": "1f74ce14-c443-4031-bcb9-ad549e30e106",
"tag": "door",
"externalId": "front",
"description": "Front door"
}
],
"fraud": {
"passed": true,
"bypassed": false,
"blocked": false,
"blockedUntil": null,
"mocked": false,
"jumped": false,
"inaccurate": false,
"countryNotAllowed": false,
"failureReasons": [],
"lastMockedAt": null,
"lastJumpedAt": null,
"lastInaccurateAt": null
},
"trip": {
"id": "7376200d-f1b3-46ff-8498-0912480cffa3",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"externalId": "order-1041",
"userId": "driver-118",
"mode": "car",
"status": "arrived",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"destinationLocation": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"approachingThreshold": 5,
"scheduledArrivalAt": "2026-11-04T16:53:20.916Z",
"metadata": {
"orderId": 1041
},
"eta": {
"duration": 0.1,
"distance": 36,
"source": "estimate"
},
"delay": {
"scheduledArrivalTimeDelay": -14.9,
"delayed": false
},
"startedAt": "2026-11-04T16:38:20.922Z",
"approachingAt": "2026-11-04T16:38:21.007Z",
"arrivedAt": "2026-11-04T16:38:21.107Z",
"endedAt": null,
"createdAt": "2026-11-04T16:38:20.922Z",
"updatedAt": "2026-11-04T16:38:21.107Z"
},
"metadata": {},
"lastSeenAt": "2026-11-04T16:43:20.936Z",
"updatedAt": "2026-11-04T16:38:21.107Z",
"createdAt": "2026-11-04T16:38:21.007Z"
},
"events": [
{
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": {
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
}
},
"place": null,
"region": null,
"beacon": null,
"trip": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"confidence": 3,
"duration": null,
"currentlyOperating": true,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:43:20.936Z",
"actualCreatedAt": "2026-11-04T16:38:21.107Z",
"id": "37c40716-16b6-44d9-8034-264092af7542",
"type": "user.entered_geofence"
},
{
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": null,
"place": {
"id": "1c8cdf41-760a-4cfe-aed0-8e0699bf4e4f",
"name": "Corner Café",
"chain": {
"slug": "corner-cafe",
"name": "Corner Café"
},
"categories": ["cafe", "food-beverage"]
},
"region": null,
"beacon": null,
"trip": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"confidence": 3,
"duration": null,
"currentlyOperating": null,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:43:20.936Z",
"actualCreatedAt": "2026-11-04T16:38:21.107Z",
"id": "3e9130f7-7498-4d09-a673-da70251c81ff",
"type": "user.entered_place"
},
{
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": null,
"place": null,
"region": null,
"beacon": {
"id": "1f74ce14-c443-4031-bcb9-ad549e30e106",
"tag": "door",
"externalId": "front",
"description": "Front door"
},
"trip": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"confidence": null,
"duration": null,
"currentlyOperating": null,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:43:20.936Z",
"actualCreatedAt": "2026-11-04T16:38:21.107Z",
"id": "a9019dc5-e7c4-40b5-8800-8cfe91d1474a",
"type": "user.entered_beacon"
},
{
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": null,
"place": null,
"region": null,
"beacon": null,
"trip": {
"id": "7376200d-f1b3-46ff-8498-0912480cffa3",
"externalId": "order-1041",
"status": "arrived",
"mode": "car",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"eta": {
"duration": 0.1,
"distance": 36,
"source": "estimate"
},
"metadata": {
"orderId": 1041
}
},
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"confidence": null,
"duration": null,
"currentlyOperating": null,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:43:20.936Z",
"actualCreatedAt": "2026-11-04T16:38:21.107Z",
"id": "e6673012-7541-49b9-85dc-322d6182c85e",
"type": "user.arrived_at_trip_destination"
}
],
"geofences": [
{
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "circle",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
},
"geometry": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"radius": 100,
"center": null,
"minutes": null,
"mode": null,
"dwellThresholdMinutes": 10,
"enabled": true,
"userIds": [],
"ip": [],
"stopDetection": true,
"operatingHours": {
"mon": [
["09:00", "21:00"]
],
"sat": [
["10:00", "18:00"]
]
},
"timezone": "America/New_York",
"currentlyOperating": true,
"disableAfter": null,
"deleteAfter": null,
"syncId": null,
"createdAt": "2026-11-04T16:38:20.771Z",
"updatedAt": "2026-11-04T16:38:20.771Z"
}
],
"place": {
"id": "1c8cdf41-760a-4cfe-aed0-8e0699bf4e4f",
"name": "Corner Café",
"chain": {
"slug": "corner-cafe",
"name": "Corner Café"
},
"categories": ["cafe", "food-beverage"]
},
"country": {
"type": "country",
"code": "US",
"name": "United States of America",
"flag": "🇺🇸",
"allowed": true
},
"state": {
"type": "state",
"code": "NY",
"name": "New York"
}
} GET /v1/context
Publishable or secret key
What's at a point: enabled geofences that contain it, the nearest place within the visit radius, and the
country and state. Stateless: nothing is stored, no events fire, and it doesn't count as a tracked user.
place is returned whether or not places are turned on for the project.
| Name | Type | Description |
|---|---|---|
coordinates required | string | latitude,longitude, e.g. 40.7411,-73.9897. Latitude first. |
curl "https://api.mapsocket.com/v1/context?coordinates=40.7411,-73.9897" -H "Authorization: pk_live_…" {
"geofences": [
{
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "circle",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
},
"geometry": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"radius": 100,
"center": null,
"minutes": null,
"mode": null,
"dwellThresholdMinutes": 10,
"enabled": true,
"userIds": [],
"ip": [],
"stopDetection": true,
"operatingHours": {
"mon": [
["09:00", "21:00"]
],
"sat": [
["10:00", "18:00"]
]
},
"timezone": "America/New_York",
"currentlyOperating": true,
"disableAfter": null,
"deleteAfter": null,
"syncId": null,
"createdAt": "2026-11-04T16:38:20.771Z",
"updatedAt": "2026-11-04T16:38:20.771Z"
}
],
"place": {
"id": "1c8cdf41-760a-4cfe-aed0-8e0699bf4e4f",
"name": "Corner Café",
"chain": {
"slug": "corner-cafe",
"name": "Corner Café"
},
"categories": ["cafe", "food-beverage"]
},
"country": {
"type": "country",
"code": "US",
"name": "United States of America",
"flag": "🇺🇸",
"allowed": true
},
"state": {
"type": "state",
"code": "NY",
"name": "New York"
}
} Geofences
Circles, polygons and isochrones (the area reachable within some minutes of travel), identified by your
tag and externalId. Each can restrict who triggers it, require a stop, keep opening
hours and switch itself off or delete itself at a set time.
GET /v1/search/geofences
Publishable or secret key
Enabled geofences near a point, nearest first. Each result adds distance: meters from the point to
the geofence's edge, rounded, 0 when inside. /v1/geofences/search is the same endpoint.
| Name | Type | Description |
|---|---|---|
near required | string | latitude,longitude to search from. |
radius optional | number | Meters, 1–50,000. Default 1000. A geofence matches if its edge is within this distance. |
tags optional | string | Comma-separated tags, e.g. store,zone. Default: all tags. |
limit optional | integer | 1–100. Default 50. |
curl "https://api.mapsocket.com/v1/search/geofences?near=40.7400,-73.9900&radius=500&tags=store,zone&limit=5" \
-H "Authorization: pk_live_…" {
"geofences": [
{
"id": "24166ed2-eebf-4608-823b-c9a147f1a224",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "polygon",
"tag": "zone",
"externalId": "z1",
"description": "Flatiron delivery zone",
"metadata": {},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-73.992, 40.738],
[-73.986, 40.738],
[-73.986, 40.743],
[-73.992, 40.743],
[-73.992, 40.738]
]
]
]
},
"radius": null,
"center": null,
"minutes": null,
"mode": null,
"dwellThresholdMinutes": null,
"enabled": true,
"userIds": ["driver-118"],
"ip": ["10.0.0.0/8"],
"stopDetection": null,
"operatingHours": null,
"timezone": "UTC",
"currentlyOperating": true,
"disableAfter": "2027-01-01T00:00:00.000Z",
"deleteAfter": null,
"syncId": null,
"createdAt": "2026-11-04T16:38:20.771Z",
"updatedAt": "2026-11-04T16:38:20.771Z",
"distance": 0
},
{
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "circle",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
},
"geometry": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"radius": 100,
"center": null,
"minutes": null,
"mode": null,
"dwellThresholdMinutes": 10,
"enabled": true,
"userIds": [],
"ip": [],
"stopDetection": true,
"operatingHours": {
"mon": [
["09:00", "21:00"]
],
"sat": [
["10:00", "18:00"]
]
},
"timezone": "America/New_York",
"currentlyOperating": true,
"disableAfter": null,
"deleteAfter": null,
"syncId": null,
"createdAt": "2026-11-04T16:38:20.771Z",
"updatedAt": "2026-11-04T16:38:20.771Z",
"distance": 45
}
]
} POST /v1/geofences/check
Publishable or secret key
Check up to 1,000 points at once: for each, whether it's inside any enabled geofence and which ones. Stateless
like context: no users, events or webhooks. Use it to assign orders or addresses to zones.
One call counts as one request against the rate limit. Results come back in the order you sent the points;
id is null for points sent without one.
| Name | Type | Description |
|---|---|---|
points required | array | 1–1,000 objects of { latitude, longitude, id? }. id is your own label, up to 128 characters, echoed back. |
tag optional | string | Only consider geofences with this tag. Default: all tags. |
curl -X POST "https://api.mapsocket.com/v1/geofences/check" \
-H "Authorization: pk_live_…" -H "Content-Type: application/json" \
-d '{
"tag": "zone",
"points": [
{ "id": "order-1041", "latitude": 40.7411, "longitude": -73.9897 },
{ "id": "order-1042", "latitude": 40.7614, "longitude": -73.9776 }
]
}' {
"results": [
{
"id": "order-1041",
"latitude": 40.7411,
"longitude": -73.9897,
"inside": true,
"geofences": [
{
"id": "24166ed2-eebf-4608-823b-c9a147f1a224",
"tag": "zone",
"externalId": "z1",
"description": "Flatiron delivery zone"
}
]
},
{
"id": "order-1042",
"latitude": 40.7614,
"longitude": -73.9776,
"inside": false,
"geofences": []
}
]
} GET /v1/geofences/:tag/:externalId/check
Publishable or secret key
Is a point inside one geofence, and if not, how far away is it? distance is meters to the edge, to
one decimal, 0 when inside. The geofence is checked even if it's disabled. 404 if
there's no such geofence.
| Name | Type | Description |
|---|---|---|
tag required | path · string | The geofence's tag. |
externalId required | path · string | The geofence's external id. |
| Name | Type | Description |
|---|---|---|
coordinates required | string | latitude,longitude to test. |
curl "https://api.mapsocket.com/v1/geofences/zone/z1/check?coordinates=40.7440,-73.9850" -H "Authorization: pk_live_…" {
"inside": false,
"distance": 212.4,
"geofence": {
"id": "24166ed2-eebf-4608-823b-c9a147f1a224",
"tag": "zone",
"externalId": "z1",
"description": "Flatiron delivery zone"
}
} GET /v1/geofences
Secret key only
The project's geofences, enabled or not, most recently updated first.
| Name | Type | Description |
|---|---|---|
tag optional | string | Only geofences with this tag. |
externalId optional | string | Only geofences with this external id. |
metadata[key] optional | string | Only geofences whose metadata key equals the value, compared as text. Repeat for several keys, e.g. metadata[region]=east. |
q optional | string | Case-insensitive match on description or external id. |
createdAftercreatedBeforeupdatedAfterupdatedBefore optional | string | ISO-8601 timestamps with an offset. |
limit optional | integer | 1–1,000. Default 100. |
curl "https://api.mapsocket.com/v1/geofences?tag=store&metadata[region]=east" -H "Authorization: sk_live_…" {
"geofences": [
{
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "circle",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
},
"geometry": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"radius": 100,
"center": null,
"minutes": null,
"mode": null,
"dwellThresholdMinutes": 10,
"enabled": true,
"userIds": [],
"ip": [],
"stopDetection": true,
"operatingHours": {
"mon": [
["09:00", "21:00"]
],
"sat": [
["10:00", "18:00"]
]
},
"timezone": "America/New_York",
"currentlyOperating": true,
"disableAfter": null,
"deleteAfter": null,
"syncId": null,
"createdAt": "2026-11-04T16:38:20.771Z",
"updatedAt": "2026-11-04T16:38:20.771Z"
}
]
} POST /v1/geofences
Secret key only
Create a geofence. Returns 201. If one with the same tag and externalId
exists, it's replaced instead, as with PUT.
| Name | Type | Description |
|---|---|---|
type required | "circle" | "polygon" | "isochrone" | Decides which geometry fields apply. |
center optional | GeoJSON Point | Circles and isochrones: the center, [lng, lat]. Required unless you send address. |
address optional | string | Circles and isochrones: geocoded to the center instead. Needs a geocoder on the server (501 geocoder_not_configured otherwise; 400 address_not_found if it can't be found). |
radius optional | number | Circles only, required. Meters, 10–100,000. |
minutesmode optional | integer, string | Isochrones only. minutes (1–60, required) of travel by mode: car (default), truck, motorbike, bike or foot. The area is computed by a routing engine when the geofence is saved; without one you get 501 routing_not_configured. |
geometry optional | GeoJSON Polygon | MultiPolygon | Polygons only, required. Rings need at least four positions. Invalid shapes are repaired; polygons are returned as MultiPolygon. |
tag required | string | Group name, 1–64 characters of A–Z a–z 0–9 _ . : -. With externalId it identifies the geofence. |
externalId required | string | Your id, 1–128 characters, unique within the tag. |
description optional | string | Up to 256 characters. Default "". |
metadata optional | object | Up to 16 keys with string, number, boolean or null values. Copied into events. Default {}. |
dwellThresholdMinutes optional | integer | null | 1–1,440. When set, a dwell event fires once per visit after this many minutes. Default null. |
enabled optional | boolean | Disabled geofences don't match tracks, context, search or batch checks. Default true. |
stopDetection optional | boolean | null | true: entry needs stopped or foreground. null (default) uses the project setting. |
userIds optional | string[] | Only these users can trigger events. Up to 1,000. Default [], everyone. |
ip optional | string[] | Only tracks from these IPs count: single IPs, wildcards like 8.8.8.*, or CIDR ranges like 10.0.0.0/8. Up to 100. Default [], any IP. |
operatingHours optional | object | null | Opening hours by day, e.g. { "mon": [["9:00 AM", "5:00 PM"]] }. Days are mon–sun or full names; times are HH:mm or h:mm AM. A close before the open runs past midnight. Returned normalised to 24-hour times. Default null, always open. |
timezone optional | string | IANA time zone for operatingHours, e.g. America/New_York. Default UTC. |
disableAfter optional | string | null | ISO-8601. After this time the geofence stops matching anything. |
deleteAfter optional | string | null | ISO-8601. The geofence is deleted within a minute after this time. |
curl -X POST "https://api.mapsocket.com/v1/geofences" \
-H "Authorization: sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"type": "circle",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"center": { "type": "Point", "coordinates": [-73.9897, 40.7411] },
"radius": 100,
"dwellThresholdMinutes": 10,
"metadata": { "region": "east" },
"stopDetection": true,
"operatingHours": { "mon": [["9:00 AM", "9:00 PM"]], "sat": [["10:00", "18:00"]] },
"timezone": "America/New_York"
}' {
"geofence": {
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "circle",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
},
"geometry": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"radius": 100,
"center": null,
"minutes": null,
"mode": null,
"dwellThresholdMinutes": 10,
"enabled": true,
"userIds": [],
"ip": [],
"stopDetection": true,
"operatingHours": {
"mon": [
["09:00", "21:00"]
],
"sat": [
["10:00", "18:00"]
]
},
"timezone": "America/New_York",
"currentlyOperating": true,
"disableAfter": null,
"deleteAfter": null,
"syncId": null,
"createdAt": "2026-11-04T16:38:20.771Z",
"updatedAt": "2026-11-04T16:38:20.771Z"
}
} GET /v1/geofences/:tag/:externalId
Secret key only
One geofence. 404 not_found if there's no such geofence.
| Name | Type | Description |
|---|---|---|
tag required | path · string | The geofence's tag. |
externalId required | path · string | The geofence's external id. |
curl "https://api.mapsocket.com/v1/geofences/store/flatiron" -H "Authorization: sk_live_…" {
"geofence": {
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "circle",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
},
"geometry": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"radius": 100,
"center": null,
"minutes": null,
"mode": null,
"dwellThresholdMinutes": 10,
"enabled": true,
"userIds": [],
"ip": [],
"stopDetection": true,
"operatingHours": {
"mon": [
["09:00", "21:00"]
],
"sat": [
["10:00", "18:00"]
]
},
"timezone": "America/New_York",
"currentlyOperating": true,
"disableAfter": null,
"deleteAfter": null,
"syncId": null,
"createdAt": "2026-11-04T16:38:20.771Z",
"updatedAt": "2026-11-04T16:38:20.771Z"
}
} PUT /v1/geofences/:tag/:externalId
Secret key only
Create or replace the geofence with this tag and external id: 201 when it's new, 200
when it replaced one. The body is the same as POST without tag and
externalId. Replacing sets every field, so omitted fields go back to their defaults.
| Name | Type | Description |
|---|---|---|
tag required | path · string | Overrides any tag in the body. |
externalId required | path · string | Overrides any externalId in the body. |
curl -X PUT "https://api.mapsocket.com/v1/geofences/zone/z1" \
-H "Authorization: sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"type": "polygon",
"description": "Flatiron delivery zone",
"geometry": {
"type": "Polygon",
"coordinates": [[[-73.992, 40.738], [-73.986, 40.738], [-73.986, 40.743], [-73.992, 40.743], [-73.992, 40.738]]]
},
"userIds": ["driver-118"],
"ip": ["10.0.0.0/8"],
"disableAfter": "2027-01-01T00:00:00Z"
}' {
"geofence": {
"id": "24166ed2-eebf-4608-823b-c9a147f1a224",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "polygon",
"tag": "zone",
"externalId": "z1",
"description": "Flatiron delivery zone",
"metadata": {},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-73.992, 40.738],
[-73.986, 40.738],
[-73.986, 40.743],
[-73.992, 40.743],
[-73.992, 40.738]
]
]
]
},
"radius": null,
"center": null,
"minutes": null,
"mode": null,
"dwellThresholdMinutes": null,
"enabled": true,
"userIds": ["driver-118"],
"ip": ["10.0.0.0/8"],
"stopDetection": null,
"operatingHours": null,
"timezone": "UTC",
"currentlyOperating": true,
"disableAfter": "2027-01-01T00:00:00.000Z",
"deleteAfter": null,
"syncId": null,
"createdAt": "2026-11-04T16:38:20.771Z",
"updatedAt": "2026-11-04T16:38:20.771Z"
}
} DELETE /v1/geofences/:tag/:externalId
Secret key only
Delete a geofence. Returns 204, or 404 not_found. Users inside it get a
user.exited_geofence with tag: "deleted" on their next track.
| Name | Type | Description |
|---|---|---|
tag required | path · string | The geofence's tag. |
externalId required | path · string | The geofence's external id. |
curl -X DELETE "https://api.mapsocket.com/v1/geofences/zone/z1" -H "Authorization: sk_live_…" HTTP/1.1 204 No Content GET /v1/geofences/:tag/:externalId/users
Secret key only
Users currently inside a geofence, as user objects.
| Name | Type | Description |
|---|---|---|
tag required | path · string | The geofence's tag. |
externalId required | path · string | The geofence's external id. |
| Name | Type | Description |
|---|---|---|
limit optional | integer | 1–1,000. Default 100. Most recently updated first. |
curl "https://api.mapsocket.com/v1/geofences/store/flatiron/users" -H "Authorization: sk_live_…" {
"users": [
{
"userId": "driver-118",
"…": "the user object"
}
]
} POST /v1/geofences/import
Secret key only
Upload many geofences at once, as GeoJSON or as a Radar-format CSV. The whole file is validated first and
written in one transaction: if any row is invalid you get a 400 naming it (Feature 3,
Row 7), and nothing is saved. Rows whose tag and external id exist replace those geofences.
Requests are limited to 25 MB, 10,000 geofences, and 200 address lookups or isochrones per file.
GeoJSON: send JSON with a featureCollection. Feature properties named like
geofence fields (enabled, dwellThreshold, userIds, ip,
stopDetection, operatingHours, timezone, disableAfter,
deleteAfter) are applied; other scalar properties become metadata (up to 16).
| Name | Type | Description |
|---|---|---|
featureCollection required | object | A GeoJSON FeatureCollection with 1–10,000 features in WGS84. Polygon and MultiPolygon features become polygons; Point features become circles with the radius property in meters (default 100). |
tag optional | string | Tag for every geofence in the file. Default imported. |
tagProperty optional | string | Take each geofence's tag from this feature property instead. |
externalIdProperty optional | string | Feature property to use as the external id. Default: the id property, else the feature's position (1, 2, …). |
descriptionProperty optional | string | Feature property to use as the description. Default: the name property. |
curl -X POST "https://api.mapsocket.com/v1/geofences/import" \
-H "Authorization: sk_live_…" -H "Content-Type: application/json" \
-d @- <<'EOF'
{
"tag": "zone",
"externalIdProperty": "zone_code",
"featureCollection": {
"type": "FeatureCollection",
"features": [
{ "type": "Feature",
"properties": { "zone_code": "z1", "name": "Flatiron delivery zone", "stopDetection": true },
"geometry": { "type": "Polygon", "coordinates": [[[-73.992, 40.738], [-73.986, 40.738], [-73.986, 40.743], [-73.992, 40.743], [-73.992, 40.738]]] } },
{ "type": "Feature",
"properties": { "zone_code": "hub-1", "name": "Hub pickup", "radius": 150 },
"geometry": { "type": "Point", "coordinates": [-73.9897, 40.7411] } }
]
}
}
EOF CSV: send the file as the body with Content-Type: text/csv, or as JSON
{ "csv": "…" }. A header row names the columns:
type, tag, externalId- Required.
typeiscircle,polygonorisochrone. coordinates | address[lng,lat]for circles and isochrones; a ring[[lng,lat],…](or a list of rings) for polygons.addressneeds a geocoder.radius, mode- Circle radius in meters. For isochrones,
radiusis the travel time in minutes andmodethe travel mode. description, enabled, stopDetection, dwellThreshold- As on the geofence. Booleans are
trueorfalse. metadata | metadata.{key}- A JSON object, or one column per key. Number and boolean cells become numbers and booleans.
operatingHours | operatingHours.{day}- A JSON object, or one column per day holding
09:00-17:00ranges separated by commas (or JSON pairs). timezone, userId(s), ip, disableAfter, deleteAfter- As on the geofence. Lists are comma-separated inside one quoted cell.
curl -X POST "https://api.mapsocket.com/v1/geofences/import" \
-H "Authorization: sk_live_…" -H "Content-Type: text/csv" \
--data-binary @- <<'EOF'
type,tag,externalId,description,coordinates,radius,metadata.region,operatingHours.mon,userIds,stopDetection
circle,store,soho,"SoHo store, Prince St","[-74.0003,40.7233]",80,east,09:00-17:00,,true
polygon,zone,z2,Downtown,"[[-74.01,40.70],[-74.00,40.70],[-74.00,40.71],[-74.01,40.71],[-74.01,40.70]]",,south,,"driver-118,driver-120",
EOF {
"imported": 2,
"created": 1,
"updated": 1
} created counts new geofences and updated replaced ones. New geofences count against the
plan's limit; a file that would go over it gets 402 plan_limit and nothing is saved. KML and
Shapefiles can be imported from the dashboard, which converts them in the browser.
The geofence object
| Field | Type | Description |
|---|---|---|
id required | uuid | MapSocket's id for the geofence. |
projectId required | uuid | |
type required | "circle" | "polygon" | "isochrone" | |
tagexternalId required | string | Your identifiers. Unique together within a project. |
description required | string | |
metadata required | object | |
geometry required | GeoJSON | Circles: the center Point. Polygons and isochrones: a MultiPolygon. |
radius required | number | null | Meters for circles, else null. |
centerminutesmode required | Point, integer, string | null | Isochrones: the center, travel minutes and mode the area was computed from. null for other types. |
dwellThresholdMinutes required | integer | null | |
enabled required | boolean | |
userIdsip required | string[] | Restrictions; empty means none. ip comes back as CIDR ranges (8.8.8.* is 8.8.8.0/24). |
stopDetection required | boolean | null | null follows the project setting. |
operatingHourstimezone required | object | null, string | Normalised to { mon: [["09:00", "17:00"]], … }. |
currentlyOperating required | boolean | Inside its operating hours right now. Always true without hours. |
disableAfterdeleteAfter required | string | null | |
syncId required | uuid | null | Set when the geofence is managed by a PostGIS sync from the dashboard. |
createdAtupdatedAt required | string |
Trips
A trip follows one user to a destination geofence: ETA on every track, an event when they're close, and another
when they arrive. Create and update trips from the app with a publishable key; list and read them with a secret
key. ETAs come from a routing engine when the server has one; otherwise they're a straight-line estimate
(distance × 1.3 at a typical speed for the mode), marked source: "estimate".
Trip stages
started- Created. The ETA is refreshed on every track.
approaching- The ETA is at or under
approachingThresholdminutes. arrived- The user is inside the destination geofence.
completed, canceled- Ended by your app (or replaced by a newer trip for the user).
expired- No update for the project's expiration time (default 24 hours).
A user has at most one active trip (started, approaching or arrived).
Ended trips can't be updated; start a new one.
POST /v1/trips
Publishable or secret key
Start a trip. Returns 201 with the trip and its user.started_trip event.
| Name | Type | Description |
|---|---|---|
externalId required | string | Your id for the trip, e.g. the order id. Reusing it restarts that trip. |
userId required | string | Who is travelling. A user has one active trip: starting a new one cancels the previous one (with user.stopped_trip). |
mode optional | string | car (default), truck, motorbike, bike or foot. Used for the ETA. |
destinationGeofenceTagdestinationGeofenceExternalId optional | string | The destination geofence. Send both or neither. 400 if there's no such geofence. |
approachingThreshold optional | integer | Minutes of ETA at which the trip becomes approaching, 1–120. Default: the project's setting (5). |
scheduledArrivalAt optional | string | ISO-8601. Enables delay. |
metadata optional | object | Copied into every trip event. Default {}. |
curl -X POST "https://api.mapsocket.com/v1/trips" \
-H "Authorization: pk_live_…" -H "Content-Type: application/json" \
-d '{
"externalId": "order-1041",
"userId": "driver-118",
"mode": "car",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"scheduledArrivalAt": "2026-11-04T16:53:20Z",
"metadata": { "orderId": 1041 }
}' {
"trip": {
"id": "7376200d-f1b3-46ff-8498-0912480cffa3",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"externalId": "order-1041",
"userId": "driver-118",
"mode": "car",
"status": "started",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"destinationLocation": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"approachingThreshold": 5,
"scheduledArrivalAt": "2026-11-04T16:53:20.916Z",
"metadata": {
"orderId": 1041
},
"eta": null,
"delay": null,
"startedAt": "2026-11-04T16:38:20.922Z",
"approachingAt": null,
"arrivedAt": null,
"endedAt": null,
"createdAt": "2026-11-04T16:38:20.922Z",
"updatedAt": "2026-11-04T16:38:20.922Z"
},
"events": [
{
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": null,
"geofence": null,
"place": null,
"region": null,
"beacon": null,
"trip": {
"id": "7376200d-f1b3-46ff-8498-0912480cffa3",
"externalId": "order-1041",
"status": "started",
"mode": "car",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"eta": null,
"metadata": {
"orderId": 1041
}
},
"location": null,
"accuracy": null,
"confidence": null,
"duration": null,
"currentlyOperating": null,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:38:20.923Z",
"actualCreatedAt": "2026-11-04T16:38:20.922Z",
"id": "f300c645-a778-4791-9b71-4cb465f4cd37",
"type": "user.started_trip"
}
]
} PATCH /v1/trips/:id
Publishable or secret key
Move a trip to a stage, or change its details. /v1/trips/:id/update is the same endpoint. The
event depends on the change: user.stopped_trip for completed or canceled,
user.arrived_at_trip_destination, user.approaching_trip_destination from
started, otherwise user.updated_trip. 400 if the trip has ended.
| Name | Type | Description |
|---|---|---|
id required | path · string | The trip's MapSocket id or your externalId. |
| Name | Type | Description |
|---|---|---|
status required | string | started, approaching, arrived, completed or canceled. completed and canceled end the trip. |
modedestinationGeofenceTagdestinationGeofenceExternalIdapproachingThresholdmetadata optional | as on create | As on create. Omitted fields keep their values. |
scheduledArrivalAt optional | string | null | null clears it. |
curl -X PATCH "https://api.mapsocket.com/v1/trips/order-1041" \
-H "Authorization: pk_live_…" -H "Content-Type: application/json" \
-d '{ "status": "completed" }' {
"trip": {
"id": "7376200d-f1b3-46ff-8498-0912480cffa3",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"externalId": "order-1041",
"userId": "driver-118",
"mode": "car",
"status": "completed",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"destinationLocation": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"approachingThreshold": 5,
"scheduledArrivalAt": "2026-11-04T16:53:20.916Z",
"metadata": {
"orderId": 1041
},
"eta": {
"duration": 0.1,
"distance": 36,
"source": "estimate"
},
"delay": {
"scheduledArrivalTimeDelay": -14.9,
"delayed": false
},
"startedAt": "2026-11-04T16:38:20.922Z",
"approachingAt": "2026-11-04T16:38:21.007Z",
"arrivedAt": "2026-11-04T16:38:21.107Z",
"endedAt": "2026-11-04T16:38:21.304Z",
"createdAt": "2026-11-04T16:38:20.922Z",
"updatedAt": "2026-11-04T16:38:21.304Z"
},
"events": [
{
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": null,
"place": null,
"region": null,
"beacon": null,
"trip": {
"id": "7376200d-f1b3-46ff-8498-0912480cffa3",
"externalId": "order-1041",
"status": "completed",
"mode": "car",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"eta": {
"duration": 0.1,
"distance": 36,
"source": "estimate"
},
"metadata": {
"orderId": 1041
}
},
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": null,
"confidence": null,
"duration": null,
"currentlyOperating": null,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:38:21.305Z",
"actualCreatedAt": "2026-11-04T16:38:21.304Z",
"id": "f28ac412-9166-4a81-b107-050f25053979",
"type": "user.stopped_trip"
}
]
} GET /v1/trips
Secret key only
The project's trips.
| Name | Type | Description |
|---|---|---|
status optional | string | Comma-separated statuses, e.g. started,approaching. |
userIdexternalId optional | string | Exact matches. |
destinationGeofenceTagdestinationGeofenceExternalId optional | string | Exact matches. |
delayed optional | "true" | Only delayed trips. |
createdAftercreatedBefore optional | string | ISO-8601 timestamps with an offset. |
limit optional | integer | 1–1,000. Default 500. Most recently updated first. |
curl "https://api.mapsocket.com/v1/trips?status=completed&userId=driver-118" -H "Authorization: sk_live_…" {
"trips": [
{
"id": "7376200d-f1b3-46ff-8498-0912480cffa3",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"externalId": "order-1041",
"userId": "driver-118",
"mode": "car",
"status": "completed",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"destinationLocation": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"approachingThreshold": 5,
"scheduledArrivalAt": "2026-11-04T16:53:20.916Z",
"metadata": {
"orderId": 1041
},
"eta": {
"duration": 0.1,
"distance": 36,
"source": "estimate"
},
"delay": {
"scheduledArrivalTimeDelay": -14.9,
"delayed": false
},
"startedAt": "2026-11-04T16:38:20.922Z",
"approachingAt": "2026-11-04T16:38:21.007Z",
"arrivedAt": "2026-11-04T16:38:21.107Z",
"endedAt": "2026-11-04T16:38:21.304Z",
"createdAt": "2026-11-04T16:38:20.922Z",
"updatedAt": "2026-11-04T16:38:21.304Z"
}
]
} GET /v1/trips/:id
Secret key only
One trip. 404 not_found if there's no such trip.
| Name | Type | Description |
|---|---|---|
id required | path · string | The trip's MapSocket id or your externalId. |
curl "https://api.mapsocket.com/v1/trips/order-1041" -H "Authorization: sk_live_…" {
"trip": {
"id": "7376200d-f1b3-46ff-8498-0912480cffa3",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"externalId": "order-1041",
"userId": "driver-118",
"mode": "car",
"status": "arrived",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"destinationLocation": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"approachingThreshold": 5,
"scheduledArrivalAt": "2026-11-04T16:53:20.916Z",
"metadata": {
"orderId": 1041
},
"eta": {
"duration": 0.1,
"distance": 36,
"source": "estimate"
},
"delay": {
"scheduledArrivalTimeDelay": -14.9,
"delayed": false
},
"startedAt": "2026-11-04T16:38:20.922Z",
"approachingAt": "2026-11-04T16:38:21.007Z",
"arrivedAt": "2026-11-04T16:38:21.107Z",
"endedAt": null,
"createdAt": "2026-11-04T16:38:20.922Z",
"updatedAt": "2026-11-04T16:38:21.107Z"
}
} DELETE /v1/trips/:id
Secret key only
Delete a trip. No event is sent. Its events stay in the event list.
| Name | Type | Description |
|---|---|---|
id required | path · string | The trip's MapSocket id or your externalId. |
curl -X DELETE "https://api.mapsocket.com/v1/trips/order-1041" -H "Authorization: sk_live_…" HTTP/1.1 204 No Content The trip object
| Field | Type | Description |
|---|---|---|
idprojectId required | uuid | |
externalIduserIdmode required | string | As created. |
status required | string | See the stages above. |
destinationGeofenceTagdestinationGeofenceExternalId required | string | null | |
destinationLocation required | GeoJSON Point | null | A circle's center, an isochrone's center, or a point on a polygon. ETAs are measured to it. |
approachingThreshold required | integer | Minutes. |
scheduledArrivalAt required | string | null | |
metadata required | object | |
eta required | object | null | { duration, distance, source }: minutes and meters to the destination. source is routing when a routing engine answered, estimate for a straight-line estimate. null until the user has a location and the trip has a destination. |
delay required | object | null | With scheduledArrivalAt: { scheduledArrivalTimeDelay, delayed }. Minutes between predicted and scheduled arrival (negative is early); delayed when over the project's delay threshold (default 10). |
startedAtapproachingAtarrivedAtendedAt required | string | null | When each stage was reached. |
createdAtupdatedAt required | string |
Places
Places are points of interest: shops, restaurants, fuel stations, airports. MapSocket uses
OpenStreetMap data (© OpenStreetMap contributors, ODbL),
loaded per area and shared by every project, plus places you add to your project. Chains come from OSM's
brand tag. When places are turned on in the project's geofencing settings, a track with
stopped: true at a place sends user.entered_place, and leaving it sends
user.exited_place. The settings can limit visits to some chains or categories.
GET /v1/search/places
Publishable or secret key
Places near a point, nearest first, OSM and your own. Each adds distance in meters.
| Name | Type | Description |
|---|---|---|
near required | string | latitude,longitude. |
radius optional | number | Meters, 1–10,000. Default 1000. |
chains optional | string | Comma-separated chain slugs, e.g. starbucks,mcdonalds. |
categories optional | string | Comma-separated categories, e.g. cafe,pharmacy. A place matches if it has any of them. |
limit optional | integer | 1–100. Default 10. |
curl "https://api.mapsocket.com/v1/search/places?near=40.7411,-73.9897&radius=200&categories=cafe" -H "Authorization: pk_live_…" {
"places": [
{
"id": "1c8cdf41-760a-4cfe-aed0-8e0699bf4e4f",
"name": "Corner Café",
"chain": {
"slug": "corner-cafe",
"name": "Corner Café"
},
"categories": ["cafe", "food-beverage"],
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"geometry": {
"type": "Point",
"coordinates": [-73.989, 40.7415]
},
"source": "custom",
"externalId": "cafe-1",
"metadata": {},
"distance": 74
}
]
} GET /v1/places
Secret key only
Your project's own places, most recently updated first. Takes limit (1–1,000, default
100). OSM places are found with search.
curl "https://api.mapsocket.com/v1/places" -H "Authorization: sk_live_…" {
"places": [
{
"id": "1c8cdf41-760a-4cfe-aed0-8e0699bf4e4f",
"name": "Corner Café",
"chain": {
"slug": "corner-cafe",
"name": "Corner Café"
},
"categories": ["cafe", "food-beverage"],
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"geometry": {
"type": "Point",
"coordinates": [-73.989, 40.7415]
},
"source": "custom",
"externalId": "cafe-1",
"metadata": {}
}
]
} POST /v1/places
Secret key only
Add a place to your project: 201 when it's new, 200 when it replaced one with the same externalId.
| Name | Type | Description |
|---|---|---|
name required | string | 1–256 characters. |
geometry required | GeoJSON Point | Polygon | MultiPolygon | A point (visits count within the project's visit radius, default 50 m) or a footprint. |
categories optional | string[] | Up to 10 of a–z 0–9 _ -. Default []. |
chain optional | object | null | { slug, name }. Default null. |
externalId optional | string | Your id. Creating a place with an existing externalId replaces it. |
metadata optional | object | Default {}. |
curl -X POST "https://api.mapsocket.com/v1/places" \
-H "Authorization: sk_live_…" -H "Content-Type: application/json" \
-d '{
"name": "Corner Café",
"externalId": "cafe-1",
"categories": ["cafe", "food-beverage"],
"chain": { "slug": "corner-cafe", "name": "Corner Café" },
"geometry": { "type": "Point", "coordinates": [-73.989, 40.7415] }
}' {
"place": {
"id": "1c8cdf41-760a-4cfe-aed0-8e0699bf4e4f",
"name": "Corner Café",
"chain": {
"slug": "corner-cafe",
"name": "Corner Café"
},
"categories": ["cafe", "food-beverage"],
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"geometry": {
"type": "Point",
"coordinates": [-73.989, 40.7415]
},
"source": "custom",
"externalId": "cafe-1",
"metadata": {}
}
} DELETE /v1/places/:id
Secret key only
Delete one of your places. OSM places can't be deleted.
| Name | Type | Description |
|---|---|---|
id required | path · string | The place's MapSocket id or your externalId. |
curl -X DELETE "https://api.mapsocket.com/v1/places/cafe-1" -H "Authorization: sk_live_…" HTTP/1.1 204 No Content The place object
| Field | Type | Description |
|---|---|---|
id required | uuid | |
projectId required | uuid | null | null for shared OpenStreetMap places. |
name required | string | |
chain required | object | null | { slug, name }. For OSM places, from the brand tag. |
categories required | string[] | OSM places: from shop, amenity, leisure, tourism, aeroway and railway tags, plus shop, food-beverage and airport groupings. |
geometry required | GeoJSON | Point or footprint. |
source required | "osm" | "custom" | |
externalId required | string | null | |
metadata required | object | OSM places keep address, website, phone and opening hours tags. |
Regions
Countries and states
Every track and context lookup resolves the country and, where boundaries exist, the state. A change sends an
exit for the old region and an entry for the new one. Boundaries come from
Natural Earth: countries worldwide, and states or provinces for
the United States, Canada, Australia, Brazil, China, India, Indonesia, Russia and South Africa. Fixes on a
coast, in a harbour or on a bridge that fall just outside the generalised boundaries match the nearest region
within 25 km. Country and state events can each be turned off in the project settings; the user's
country and state are kept up to date regardless.
Countries also feed fraud checks: with an allow or block list in the project settings,
allowed is false for excluded countries and the track fails with
country_not_allowed.
| Field | Type | Description |
|---|---|---|
type required | "country" | "state" | |
code required | string | ISO 3166-1 alpha-2 for countries (US); the postal or ISO 3166-2 suffix for states (NY). |
name required | string | |
flag optional | string | null | Countries only: the flag emoji. |
allowed optional | boolean | Countries only: false when the project's allow or block list excludes it. |
Beacons
Register iBeacon and Eddystone beacons, then have your app send the beacons it ranges in each
track's beacons list. MapSocket matches them to your enabled beacons and sends
user.entered_beacon and user.exited_beacon as the list changes. Beacon state only
changes on tracks that include beacons.
GET /v1/beacons
Secret key only
The project's beacons, newest first.
| Name | Type | Description |
|---|---|---|
tagexternalId optional | string | Exact matches. |
limit optional | integer | 1–1,000. Default 100. |
curl "https://api.mapsocket.com/v1/beacons?tag=door" -H "Authorization: sk_live_…" {
"beacons": [
{
"id": "1f74ce14-c443-4031-bcb9-ad549e30e106",
"tag": "door",
"externalId": "front",
"description": "Front door",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "ibeacon",
"uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
"major": "100",
"minor": "1",
"uid": null,
"instance": null,
"location": null,
"metadata": {},
"enabled": true,
"createdAt": "2026-11-04T16:38:20.901Z",
"updatedAt": "2026-11-04T16:38:20.901Z"
}
]
} POST /v1/beacons
Secret key only
Register a beacon: 201 when it's new. An existing beacon with the same tag and external id is
replaced, with 200.
| Name | Type | Description |
|---|---|---|
type required | "ibeacon" | "eddystone" | |
uuidmajorminor optional | string | iBeacon only, required. uuid is a UUID; major and minor may be numbers or strings. |
uidinstance optional | string | Eddystone only, required. uid is the 10-byte namespace as 20 hex characters; instance is 6 bytes as 12 hex characters. |
tagexternalId required | string | Same rules as geofences. In the PUT path, taken from the URL. |
description optional | string | Up to 256 characters. |
metadata optional | object | Up to 16 keys. |
location optional | GeoJSON Point | null | Where it's installed. Informational. |
enabled optional | boolean | Disabled beacons are ignored in tracks. Default true. |
curl -X POST "https://api.mapsocket.com/v1/beacons" \
-H "Authorization: sk_live_…" -H "Content-Type: application/json" \
-d '{
"type": "ibeacon",
"tag": "door",
"externalId": "front",
"description": "Front door",
"uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
"major": 100,
"minor": 1
}' {
"beacon": {
"id": "1f74ce14-c443-4031-bcb9-ad549e30e106",
"tag": "door",
"externalId": "front",
"description": "Front door",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "ibeacon",
"uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
"major": "100",
"minor": "1",
"uid": null,
"instance": null,
"location": null,
"metadata": {},
"enabled": true,
"createdAt": "2026-11-04T16:38:20.901Z",
"updatedAt": "2026-11-04T16:38:20.901Z"
}
} GET /v1/beacons/:tag/:externalId
Secret key only
One beacon, or 404 not_found.
| Name | Type | Description |
|---|---|---|
tag required | path · string | The beacon's tag. |
externalId required | path · string | The beacon's external id. |
curl "https://api.mapsocket.com/v1/beacons/door/front" -H "Authorization: sk_live_…" {
"beacon": {
"id": "1f74ce14-c443-4031-bcb9-ad549e30e106",
"tag": "door",
"externalId": "front",
"description": "Front door",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "ibeacon",
"uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
"major": "100",
"minor": "1",
"uid": null,
"instance": null,
"location": null,
"metadata": {},
"enabled": true,
"createdAt": "2026-11-04T16:38:20.901Z",
"updatedAt": "2026-11-04T16:38:20.901Z"
}
} PUT /v1/beacons/:tag/:externalId
Secret key only
Create or replace a beacon by tag and external id: 201 when new, 200 when replaced.
| Name | Type | Description |
|---|---|---|
tag required | path · string | The beacon's tag. |
externalId required | path · string | The beacon's external id. |
curl -X PUT "https://api.mapsocket.com/v1/beacons/door/front" \
-H "Authorization: sk_live_…" -H "Content-Type: application/json" \
-d '{ "type": "ibeacon", "uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e", "major": 100, "minor": 1, "description": "Front door" }' {
"beacon": {
"id": "1f74ce14-c443-4031-bcb9-ad549e30e106",
"tag": "door",
"externalId": "front",
"description": "Front door",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"type": "ibeacon",
"uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
"major": "100",
"minor": "1",
"uid": null,
"instance": null,
"location": null,
"metadata": {},
"enabled": true,
"createdAt": "2026-11-04T16:38:20.901Z",
"updatedAt": "2026-11-04T16:38:20.901Z"
}
} DELETE /v1/beacons/:tag/:externalId
Secret key only
Delete a beacon. Users who had it in range get a user.exited_beacon with tag: "deleted" on their next track that sends beacons.
| Name | Type | Description |
|---|---|---|
tag required | path · string | The beacon's tag. |
externalId required | path · string | The beacon's external id. |
curl -X DELETE "https://api.mapsocket.com/v1/beacons/door/front" -H "Authorization: sk_live_…" HTTP/1.1 204 No Content The beacon object
| Field | Type | Description |
|---|---|---|
idprojectId required | uuid | |
tagexternalIddescription required | string | |
type required | "ibeacon" | "eddystone" | |
uuidmajorminor required | string | null | iBeacon identifiers, null for Eddystone. Stored lower-case; matching ignores case. |
uidinstance required | string | null | Eddystone identifiers, null for iBeacon. |
location required | GeoJSON Point | null | |
metadata required | object | |
enabled required | boolean | |
createdAtupdatedAt required | string |
Events
Event types
user.entered_geofence- The user is now inside a geofence they weren't in. With stop detection, only once they're stopped or the app is in the foreground.
confidenceis 1–3. user.exited_geofence- The user was inside and isn't now.
durationis the minutes inside. A deleted geofence still sends an exit, withtag: "deleted"; a disabled, expired or restricted one is dropped quietly. user.dwelled_in_geofence- Once per visit, on the first track at least
dwellThresholdMinutesafter entry.durationis the minutes inside so far. Needs the project's dwell events setting (on by default). user.entered_place- The user stopped (
stopped: true) at a place. Needs places turned on for the project.confidenceis 3 when accuracy is within the visit radius, else 2. user.exited_place- The user left the place they were at.
durationis the minutes there. user.entered_region_country- The user's country changed (or was seen for the first time).
regionis the new country. user.exited_region_country- Sent with the entry above when the country changes.
regionis the old country. user.entered_region_state- The state or province changed, for countries with state boundaries.
user.exited_region_state- Sent with the entry above.
regionis the old state. user.entered_beacon- A registered beacon appeared in the
beaconslist the app sent. user.exited_beacon- A beacon from the previous
beaconslist isn't in this one. user.started_trip- A trip was created (or restarted with the same
externalId). user.updated_trip- Every track while the user has an active trip, and trip updates that don't change the stage. Not sent to webhooks unless you select it.
user.approaching_trip_destination- A
startedtrip's ETA dropped toapproachingThresholdminutes or less, or the app setapproaching. user.arrived_at_trip_destination- The user is inside the destination geofence, or the app set
arrived. user.arrived_at_wrong_trip_destination- The user entered a geofence with the destination's tag but a different
externalId. Off unless the project turns it on. user.stopped_trip- The trip was completed or canceled, including when a new trip for the same user replaced it.
GET /v1/events
Secret key only
The project's events.
| Name | Type | Description |
|---|---|---|
types optional | string | Comma-separated event types. type also works. |
userId optional | string | One user's events. |
geofenceTaggeofenceExternalId optional | string | Events for geofences with this tag and/or external id. |
tripId optional | uuid | One trip's events. |
verification optional | string | unverified, accepted or rejected. |
createdAftercreatedBefore optional | string | ISO-8601 with an offset. Compared with the event's createdAt. |
limit optional | integer | 1–1,000. Default 100. Newest first. |
curl "https://api.mapsocket.com/v1/events?types=user.entered_geofence,user.entered_place&userId=driver-118" -H "Authorization: sk_live_…" {
"events": [
{
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": {
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
}
},
"place": null,
"region": null,
"beacon": null,
"trip": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"confidence": 3,
"duration": null,
"currentlyOperating": true,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:43:20.936Z",
"actualCreatedAt": "2026-11-04T16:38:21.107Z",
"id": "37c40716-16b6-44d9-8034-264092af7542",
"type": "user.entered_geofence"
},
{
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": null,
"place": {
"id": "1c8cdf41-760a-4cfe-aed0-8e0699bf4e4f",
"name": "Corner Café",
"chain": {
"slug": "corner-cafe",
"name": "Corner Café"
},
"categories": ["cafe", "food-beverage"]
},
"region": null,
"beacon": null,
"trip": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"confidence": 3,
"duration": null,
"currentlyOperating": null,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:43:20.936Z",
"actualCreatedAt": "2026-11-04T16:38:21.107Z",
"id": "3e9130f7-7498-4d09-a673-da70251c81ff",
"type": "user.entered_place"
}
]
} GET /v1/events/:id
Secret key only
One event, or 404 not_found.
| Name | Type | Description |
|---|---|---|
id required | path · uuid | The event id. |
curl "https://api.mapsocket.com/v1/events/37c40716-16b6-44d9-8034-264092af7542" -H "Authorization: sk_live_…" {
"event": {
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": {
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
}
},
"place": null,
"region": null,
"beacon": null,
"trip": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"confidence": 3,
"duration": null,
"currentlyOperating": true,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:43:20.936Z",
"actualCreatedAt": "2026-11-04T16:38:21.107Z",
"id": "37c40716-16b6-44d9-8034-264092af7542",
"type": "user.entered_geofence"
}
} DELETE /v1/events/:id
Secret key only
Delete one event.
| Name | Type | Description |
|---|---|---|
id required | path · uuid | The event id. |
curl -X DELETE "https://api.mapsocket.com/v1/events/37c40716-16b6-44d9-8034-264092af7542" -H "Authorization: sk_live_…" HTTP/1.1 204 No Content POST /v1/events/:id/verification
Secret key only
Record whether an event really happened, for example after a customer confirms they're at the store. Stored on
the event as verification and verifiedAt.
| Name | Type | Description |
|---|---|---|
id required | path · uuid | The event id. |
| Name | Type | Description |
|---|---|---|
verification required | string | accepted, rejected or unverified (clears verifiedAt). |
curl -X POST "https://api.mapsocket.com/v1/events/37c40716-16b6-44d9-8034-264092af7542/verification" \
-H "Authorization: sk_live_…" -H "Content-Type: application/json" \
-d '{ "verification": "accepted" }' {
"event": {
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": {
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
}
},
"place": null,
"region": null,
"beacon": null,
"trip": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"confidence": 3,
"duration": null,
"currentlyOperating": true,
"verification": "accepted",
"verifiedAt": "2026-11-04T16:38:21.329Z",
"createdAt": "2026-11-04T16:43:20.936Z",
"actualCreatedAt": "2026-11-04T16:38:21.107Z",
"id": "37c40716-16b6-44d9-8034-264092af7542",
"type": "user.entered_geofence"
}
} The event object
| Field | Type | Description |
|---|---|---|
idprojectId required | uuid | |
type required | string | One of the event types above. |
userId required | string | |
deviceId required | string | null | |
geofence required | object | null | Geofence events: id, tag, externalId, description, metadata at event time. A geofence deleted before the exit has tag: "deleted". |
place required | object | null | Place events: id, name, chain, categories. |
region required | object | null | Region events: a region object. |
beacon required | object | null | Beacon events: id, tag, externalId, description. |
trip required | object | null | Trip events: id, externalId, status, mode, destination, eta and metadata at event time. |
location required | GeoJSON Point | null | The position that produced the event. Trip events sent from create or update use the user's last location, or null if they've never been tracked. |
accuracy required | number | null | As sent to /v1/track. |
confidence required | 1 | 2 | 3 | null | Geofence and place entries and dwells. See confidence above. |
duration required | number | null | Minutes inside, for geofence exits and dwells and place exits. |
currentlyOperating required | boolean | null | Geofence events: whether the geofence was inside its operating hours. |
verification required | string | unverified, accepted or rejected. |
verifiedAt required | string | null | |
createdAt required | string | When the location was recorded (the track's recordedAt, or server time). |
actualCreatedAt required | string | When the server received it. |
Users
A user is created by their first track (or block) and holds their current state.
GET /v1/users
Secret key only
The project's tracked users.
| Name | Type | Description |
|---|---|---|
q optional | string | Case-insensitive match on userId or description. |
updatedAfterupdatedBefore optional | string | ISO-8601 with an offset. |
limit optional | integer | 1–1,000. Default 100. Most recently updated first. |
curl "https://api.mapsocket.com/v1/users?q=driver&limit=20" -H "Authorization: sk_live_…" {
"users": [
{
"userId": "driver-118",
"…": "the user object"
}
]
} GET /v1/search/users
Secret key only
Users whose last location is near a point. Each adds distance in meters.
| Name | Type | Description |
|---|---|---|
near required | string | latitude,longitude. |
radius optional | number | Meters, 1–50,000. Default 1000. |
updatedAfter optional | string | ISO-8601. Default: 7 days ago. |
limit optional | integer | 1–100. Default 50. Nearest first. |
curl "https://api.mapsocket.com/v1/search/users?near=40.7411,-73.9897&radius=500" -H "Authorization: sk_live_…" {
"users": [
{
"userId": "driver-118",
"…": "the user object",
"distance": 28
}
]
} GET /v1/users/:userId
Secret key only
One user. 404 not_found if they've never been tracked in this project.
| Name | Type | Description |
|---|---|---|
userId required | path · string | The userId you sent to /v1/track. |
curl "https://api.mapsocket.com/v1/users/driver-118" -H "Authorization: sk_live_…" {
"user": {
"id": "dae33c61-be99-4f50-9680-2d192984da7c",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"description": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"stopped": true,
"foreground": null,
"deviceType": "iOS",
"deviceOS": "18.1",
"deviceMake": "Apple",
"deviceModel": "iPhone16,1",
"geofenceIds": ["6863c6f2-cce7-4a8c-9acc-31641cd0678a"],
"geofences": [
{
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
}
}
],
"place": {
"id": "1c8cdf41-760a-4cfe-aed0-8e0699bf4e4f",
"name": "Corner Café",
"chain": {
"slug": "corner-cafe",
"name": "Corner Café"
},
"categories": ["cafe", "food-beverage"]
},
"country": {
"type": "country",
"code": "US",
"name": "United States of America",
"flag": "🇺🇸",
"allowed": true
},
"state": {
"type": "state",
"code": "NY",
"name": "New York"
},
"beacons": [
{
"id": "1f74ce14-c443-4031-bcb9-ad549e30e106",
"tag": "door",
"externalId": "front",
"description": "Front door"
}
],
"fraud": {
"passed": true,
"bypassed": false,
"blocked": false,
"blockedUntil": null,
"mocked": false,
"jumped": false,
"inaccurate": false,
"countryNotAllowed": false,
"failureReasons": [],
"lastMockedAt": null,
"lastJumpedAt": null,
"lastInaccurateAt": null
},
"trip": {
"id": "7376200d-f1b3-46ff-8498-0912480cffa3",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"externalId": "order-1041",
"userId": "driver-118",
"mode": "car",
"status": "arrived",
"destinationGeofenceTag": "store",
"destinationGeofenceExternalId": "flatiron",
"destinationLocation": {
"type": "Point",
"coordinates": [-73.9897, 40.7411]
},
"approachingThreshold": 5,
"scheduledArrivalAt": "2026-11-04T16:53:20.916Z",
"metadata": {
"orderId": 1041
},
"eta": {
"duration": 0.1,
"distance": 36,
"source": "estimate"
},
"delay": {
"scheduledArrivalTimeDelay": -14.9,
"delayed": false
},
"startedAt": "2026-11-04T16:38:20.922Z",
"approachingAt": "2026-11-04T16:38:21.007Z",
"arrivedAt": "2026-11-04T16:38:21.107Z",
"endedAt": null,
"createdAt": "2026-11-04T16:38:20.922Z",
"updatedAt": "2026-11-04T16:38:21.107Z"
},
"metadata": {},
"lastSeenAt": "2026-11-04T16:43:20.936Z",
"updatedAt": "2026-11-04T16:38:21.107Z",
"createdAt": "2026-11-04T16:38:21.007Z"
}
} DELETE /v1/users/:userId
Secret key only
Delete a user with their location history: all their events and trips go too. Use it for privacy requests. A later track starts a new user.
| Name | Type | Description |
|---|---|---|
userId required | path · string | The userId you sent to /v1/track. |
curl -X DELETE "https://api.mapsocket.com/v1/users/driver-118" -H "Authorization: sk_live_…" HTTP/1.1 204 No Content The user object
| Field | Type | Description |
|---|---|---|
idprojectId required | uuid | |
userId required | string | Your id. |
deviceIddescription required | string | null | |
location required | GeoJSON Point | null | Last tracked position, including inaccurate fixes. |
accuracy required | number | null | |
stoppedforeground required | boolean | null | From the last track. |
deviceTypedeviceOSdeviceMakedeviceModel required | string | null | |
geofenceIds required | uuid[] | Geofences the user is inside now. |
geofences required | object[] | The same geofences: id, tag, externalId, description, metadata. |
place required | object | null | The place the user is visiting. |
countrystate required | object | null | Region objects. |
beacons required | object[] | Beacons in range at the last track that sent beacons. |
fraud required | object | See Fraud below. |
trip required | object | null | The active trip, if any. |
metadata required | object | |
lastSeenAt required | string | The last track's recordedAt. |
updatedAtcreatedAt required | string | Server times. |
Fraud
Each track is checked for location spoofing, and the result is stored as the user's fraud. Check
user.fraud.passed before you act on a location, such as unlocking a reward on entry. Thresholds and
country lists are project settings; set suppress events when failed to stop failing fixes from changing
geofences, places, regions, beacons and trips at all.
Checks and reasons
fraud_jumped_single_device- Moved more than 5 km since the previous fix, faster than the project's maximum speed (default 1,200 km/h), on the same device.
fraud_jumped_multiple_devices- The same, with a different
deviceIdthan the previous fix. fraud_mocked_from_mock_provider- The track had
mocked: true. fraud_inaccurate_exceeded_accuracy_thresholdaccuracyworse than the project's threshold (default 1,000 m).country_not_allowed- The country is on the project's block list, or not on a non-empty allow list.
user_blocked- Blocked with
/v1/fraud/block(or in the dashboard) and the block hasn't expired.
{
"passed": false,
"bypassed": false,
"blocked": false,
"blockedUntil": null,
"mocked": true,
"jumped": true,
"inaccurate": false,
"countryNotAllowed": false,
"failureReasons": ["fraud_jumped_single_device", "fraud_mocked_from_mock_provider"],
"lastMockedAt": "2026-11-04T16:48:20.936Z",
"lastJumpedAt": "2026-11-04T16:48:20.936Z",
"lastInaccurateAt": null
} | Field | Type | Description |
|---|---|---|
passed required | boolean | Every check passed (or the user is bypassed), and the user isn't blocked. |
bypassed required | boolean | Checks are skipped for this user. Set in the dashboard, for test devices. |
blockedblockedUntil required | boolean, string | null | |
mockedjumpedinaccuratecountryNotAllowed required | boolean | The checks, for the last track. |
failureReasons required | string[] | Reason codes for the last track. |
lastMockedAtlastJumpedAtlastInaccurateAt required | string | null | When each check last failed. |
POST /v1/fraud/block
Secret key only
Block users. Their tracks fail with user_blocked until the block expires or is lifted.
| Name | Type | Description |
|---|---|---|
items required | array | 1–500 of { userId, durationMs?, description? }. Without durationMs the block lasts until you unblock. Users not tracked yet are created, so they're blocked from their first track. |
curl -X POST "https://api.mapsocket.com/v1/fraud/block" \
-H "Authorization: sk_live_…" -H "Content-Type: application/json" \
-d '{ "items": [{ "userId": "driver-118", "durationMs": 3600000, "description": "Spoofing" }] }' {
"blocked": 1
} POST /v1/fraud/unblock
Secret key only
Lift blocks.
| Name | Type | Description |
|---|---|---|
items required | array | 1–500 of { userId }. unblocked counts the users that exist. |
curl -X POST "https://api.mapsocket.com/v1/fraud/unblock" \
-H "Authorization: sk_live_…" -H "Content-Type: application/json" \
-d '{ "items": [{ "userId": "driver-118" }] }' {
"unblocked": 1
} Webhooks
Add an endpoint URL in the dashboard and choose its event types. MapSocket sends a POST for each
matching event. The endpoint's signing secret is shown once, when you create it.
Event types
Any of the event types. A new endpoint receives all of them except
user.updated_trip, which fires on every track during a trip; select it if you want live ETAs.
Payload
The body wraps an event and the user as they are after
the update that produced it (user can be null for a trip created for a user who has
never been tracked). id is stable across retries of the same event; use it to skip duplicates.
Test deliveries from the dashboard have "id": "evt_test", "test": true and no
user.
POST /hooks/mapsocket HTTP/1.1
Content-Type: application/json
User-Agent: MapSocket-Webhooks/1.0
MapSocket-Event: user.entered_geofence
MapSocket-Signature: t=1793801560,v1=5f0c…e91a {
"id": "evt_37c4071616b644d98034264092af7542",
"type": "user.entered_geofence",
"createdAt": "2026-11-04T16:43:20.936Z",
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"event": {
"projectId": "6f40915a-db90-49cc-be5c-0b6902870cab",
"userId": "driver-118",
"deviceId": "ios-7F3A",
"geofence": {
"id": "6863c6f2-cce7-4a8c-9acc-31641cd0678a",
"tag": "store",
"externalId": "flatiron",
"description": "Flatiron store",
"metadata": {
"region": "east"
}
},
"place": null,
"region": null,
"beacon": null,
"trip": null,
"location": {
"type": "Point",
"coordinates": [-73.9895, 40.7413]
},
"accuracy": 12,
"confidence": 3,
"duration": null,
"currentlyOperating": true,
"verification": "unverified",
"verifiedAt": null,
"createdAt": "2026-11-04T16:43:20.936Z",
"actualCreatedAt": "2026-11-04T16:38:21.107Z",
"id": "37c40716-16b6-44d9-8034-264092af7542",
"type": "user.entered_geofence"
},
"user": {
"userId": "driver-118",
"…": "the user object after this update"
}
} Signature
Every delivery carries:
MapSocket-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + '.' + body)>
To verify, take t from the header, compute the HMAC-SHA256 of t, a period and the raw
request body with your endpoint's secret, and compare it with v1 in constant time. Reject
timestamps more than a few minutes old to stop replays. Verify the raw bytes: parsing and re-serializing the
JSON changes them.
Delivery and retries
- Any
2xxresponse within 5 seconds counts as delivered. Redirects aren't followed and count as failures. - A failed delivery is tried up to 3 times in all: immediately, 10 seconds later, then 60 seconds after that.
- Each attempt, with its status code and timing, is listed under the endpoint in the dashboard.
- Deliveries for different events can arrive out of order. Sort by
createdAtif order matters.
Verify in Node
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.MAPSOCKET_WEBHOOK_SECRET;
/** True when the header was made from this exact body with our secret, within 5 minutes. */
function verify(rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=", 2)));
const t = Number(parts.t);
if (!parts.v1 || !Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(parts.v1, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Use the raw body: the signature covers the exact bytes we sent.
app.post("/hooks/mapsocket", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verify(raw, req.get("MapSocket-Signature") ?? "")) return res.status(400).send("bad signature");
const { id, type, event, user } = JSON.parse(raw);
// Retries can repeat a delivery: skip ids you've already handled.
console.log(id, type, event.userId, event.geofence?.externalId, user?.fraud.passed);
res.sendStatus(204);
});
app.listen(3000);