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 ways to send a key
# 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.

403 response
{
  "error": {
    "code": "secret_key_required",
    "message": "This endpoint needs a secret key (sk_live_…). Never ship secret keys to browsers or apps."
  }
}

Status codes

StatusError codesWhen
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 204 and 304. TileJSON and event streams don't count. On the Sandbox, tiles stop with 429 monthly_quota_exceeded after 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/track this month. A new user past the cap gets 402 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.

Parameters
NameTypeDescription
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.

Request
curl "https://tiles.mapsocket.com/t/acme/parcels.json?key=pk_live_…"
Response · 200
{
  "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.

Parameters
NameTypeDescription
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.
503
source_paused or source_unavailable (still building, or the last build failed).
429
rate_limited, or monthly_quota_exceeded on the Sandbox.
Conditional request
curl -i "https://tiles.mapsocket.com/t/acme/parcels/14/4823/6160.pbf?key=pk_live_…" \
  -H 'If-None-Match: "…"'
Response · 304
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.

Parameters
NameTypeDescription
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.
Events
EventSent whenData 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
Stream
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.

map.js
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.

Query parameters
NameTypeDescription
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).
Request
curl "https://tiles.mapsocket.com/t/terrain/contours.json?key=pk_live_…&units=m"
Response · 200
{
  "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.

Feature properties
NameTypeDescription
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.
Spacing by zoom (minor / index)
NameTypeDescription
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.

Query parameters
NameTypeDescription
locations required string 1–100 latitude,longitude pairs separated by |.
units optional string m (default) or ft.
Request
curl "https://api.mapsocket.com/v1/elevation?locations=45.8326,6.8652|38.7139,-9.1334&units=m" \
  -H "Authorization: pk_live_…"
Response · 200
{
  "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.js
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.

Request
curl "https://tiles.mapsocket.com/t/georef/3b1f0a52-6c1e-4f3e-9a7d-2f4c8e1b9d60.json?key=pk_live_…"
Response · 200
{
  "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.js
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 accuracy of the edge) and exits (leave only when more than accuracy outside). Geofences limited to userIds or ip ranges only exist for those users and IPs.
  • Stop detection. When a geofence (or the project) uses it, entry waits until a track has stopped: true or foreground: 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 currentlyOperating either way.
  • Places need stopped: true to 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 accuracy of 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.

Body
NameTypeDescription
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.
Request
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 }]
  }'
Response · 200
{
  "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.

Query parameters
NameTypeDescription
coordinates required string latitude,longitude, e.g. 40.7411,-73.9897. Latitude first.
Request
curl "https://api.mapsocket.com/v1/context?coordinates=40.7411,-73.9897" -H "Authorization: pk_live_…"
Response · 200
{
  "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.

Query parameters
NameTypeDescription
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.
Request
curl "https://api.mapsocket.com/v1/search/geofences?near=40.7400,-73.9900&radius=500&tags=store,zone&limit=5" \
  -H "Authorization: pk_live_…"
Response · 200
{
  "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.

Body
NameTypeDescription
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.
Request
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 }
    ]
  }'
Response · 200
{
  "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.

Path parameters
NameTypeDescription
tag required path · string The geofence's tag.
externalId required path · string The geofence's external id.
Query parameters
NameTypeDescription
coordinates required string latitude,longitude to test.
Request
curl "https://api.mapsocket.com/v1/geofences/zone/z1/check?coordinates=40.7440,-73.9850" -H "Authorization: pk_live_…"
Response · 200
{
  "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.

Query parameters
NameTypeDescription
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.
Request
curl "https://api.mapsocket.com/v1/geofences?tag=store&metadata[region]=east" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Body
NameTypeDescription
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.
Request
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"
  }'
Response · 201
{
  "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.

Path parameters
NameTypeDescription
tag required path · string The geofence's tag.
externalId required path · string The geofence's external id.
Request
curl "https://api.mapsocket.com/v1/geofences/store/flatiron" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Path parameters
NameTypeDescription
tag required path · string Overrides any tag in the body.
externalId required path · string Overrides any externalId in the body.
Request
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"
  }'
Response · 201
{
  "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.

Path parameters
NameTypeDescription
tag required path · string The geofence's tag.
externalId required path · string The geofence's external id.
Request
curl -X DELETE "https://api.mapsocket.com/v1/geofences/zone/z1" -H "Authorization: sk_live_…"
Response · 204
HTTP/1.1 204 No Content

GET /v1/geofences/:tag/:externalId/users

Secret key only

Users currently inside a geofence, as user objects.

Path parameters
NameTypeDescription
tag required path · string The geofence's tag.
externalId required path · string The geofence's external id.
Query parameters
NameTypeDescription
limit optional integer 1–1,000. Default 100. Most recently updated first.
Request
curl "https://api.mapsocket.com/v1/geofences/store/flatiron/users" -H "Authorization: sk_live_…"
Response · 200
{
  "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).

GeoJSON body
NameTypeDescription
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.
GeoJSON request
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. type is circle, polygon or isochrone.
coordinates | address
[lng,lat] for circles and isochrones; a ring [[lng,lat],…] (or a list of rings) for polygons. address needs a geocoder.
radius, mode
Circle radius in meters. For isochrones, radius is the travel time in minutes and mode the travel mode.
description, enabled, stopDetection, dwellThreshold
As on the geofence. Booleans are true or false.
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:00 ranges separated by commas (or JSON pairs).
timezone, userId(s), ip, disableAfter, deleteAfter
As on the geofence. Lists are comma-separated inside one quoted cell.
CSV request
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
Response · 200
{
  "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

Geofence
FieldTypeDescription
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 approachingThreshold minutes.
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.

Body
NameTypeDescription
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 {}.
Request
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 }
  }'
Response · 201
{
  "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.

Path parameters
NameTypeDescription
id required path · string The trip's MapSocket id or your externalId.
Body
NameTypeDescription
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.
Request
curl -X PATCH "https://api.mapsocket.com/v1/trips/order-1041" \
  -H "Authorization: pk_live_…" -H "Content-Type: application/json" \
  -d '{ "status": "completed" }'
Response · 200
{
  "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.

Query parameters
NameTypeDescription
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.
Request
curl "https://api.mapsocket.com/v1/trips?status=completed&userId=driver-118" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Path parameters
NameTypeDescription
id required path · string The trip's MapSocket id or your externalId.
Request
curl "https://api.mapsocket.com/v1/trips/order-1041" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Path parameters
NameTypeDescription
id required path · string The trip's MapSocket id or your externalId.
Request
curl -X DELETE "https://api.mapsocket.com/v1/trips/order-1041" -H "Authorization: sk_live_…"
Response · 204
HTTP/1.1 204 No Content

The trip object

Trip
FieldTypeDescription
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.

Query parameters
NameTypeDescription
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.
Request
curl "https://api.mapsocket.com/v1/search/places?near=40.7411,-73.9897&radius=200&categories=cafe" -H "Authorization: pk_live_…"
Response · 200
{
  "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.

Request
curl "https://api.mapsocket.com/v1/places" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Body
NameTypeDescription
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 {}.
Request
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] }
  }'
Response · 201
{
  "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.

Path parameters
NameTypeDescription
id required path · string The place's MapSocket id or your externalId.
Request
curl -X DELETE "https://api.mapsocket.com/v1/places/cafe-1" -H "Authorization: sk_live_…"
Response · 204
HTTP/1.1 204 No Content

The place object

Place
FieldTypeDescription
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.

Region
FieldTypeDescription
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.

Query parameters
NameTypeDescription
tagexternalId optional string Exact matches.
limit optional integer 1–1,000. Default 100.
Request
curl "https://api.mapsocket.com/v1/beacons?tag=door" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Body
NameTypeDescription
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.
Request
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
  }'
Response · 201
{
  "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.

Path parameters
NameTypeDescription
tag required path · string The beacon's tag.
externalId required path · string The beacon's external id.
Request
curl "https://api.mapsocket.com/v1/beacons/door/front" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Path parameters
NameTypeDescription
tag required path · string The beacon's tag.
externalId required path · string The beacon's external id.
Request
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" }'
Response · 200
{
  "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.

Path parameters
NameTypeDescription
tag required path · string The beacon's tag.
externalId required path · string The beacon's external id.
Request
curl -X DELETE "https://api.mapsocket.com/v1/beacons/door/front" -H "Authorization: sk_live_…"
Response · 204
HTTP/1.1 204 No Content

The beacon object

Beacon
FieldTypeDescription
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. confidence is 1–3.
user.exited_geofence
The user was inside and isn't now. duration is the minutes inside. A deleted geofence still sends an exit, with tag: "deleted"; a disabled, expired or restricted one is dropped quietly.
user.dwelled_in_geofence
Once per visit, on the first track at least dwellThresholdMinutes after entry. duration is 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. confidence is 3 when accuracy is within the visit radius, else 2.
user.exited_place
The user left the place they were at. duration is the minutes there.
user.entered_region_country
The user's country changed (or was seen for the first time). region is the new country.
user.exited_region_country
Sent with the entry above when the country changes. region is 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. region is the old state.
user.entered_beacon
A registered beacon appeared in the beacons list the app sent.
user.exited_beacon
A beacon from the previous beacons list 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 started trip's ETA dropped to approachingThreshold minutes or less, or the app set approaching.
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.

Query parameters
NameTypeDescription
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.
Request
curl "https://api.mapsocket.com/v1/events?types=user.entered_geofence,user.entered_place&userId=driver-118" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Path parameters
NameTypeDescription
id required path · uuid The event id.
Request
curl "https://api.mapsocket.com/v1/events/37c40716-16b6-44d9-8034-264092af7542" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Path parameters
NameTypeDescription
id required path · uuid The event id.
Request
curl -X DELETE "https://api.mapsocket.com/v1/events/37c40716-16b6-44d9-8034-264092af7542" -H "Authorization: sk_live_…"
Response · 204
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.

Path parameters
NameTypeDescription
id required path · uuid The event id.
Body
NameTypeDescription
verification required string accepted, rejected or unverified (clears verifiedAt).
Request
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" }'
Response · 200
{
  "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

Event
FieldTypeDescription
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.

Query parameters
NameTypeDescription
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.
Request
curl "https://api.mapsocket.com/v1/users?q=driver&limit=20" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Query parameters
NameTypeDescription
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.
Request
curl "https://api.mapsocket.com/v1/search/users?near=40.7411,-73.9897&radius=500" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Path parameters
NameTypeDescription
userId required path · string The userId you sent to /v1/track.
Request
curl "https://api.mapsocket.com/v1/users/driver-118" -H "Authorization: sk_live_…"
Response · 200
{
  "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.

Path parameters
NameTypeDescription
userId required path · string The userId you sent to /v1/track.
Request
curl -X DELETE "https://api.mapsocket.com/v1/users/driver-118" -H "Authorization: sk_live_…"
Response · 204
HTTP/1.1 204 No Content

The user object

Tracked user
FieldTypeDescription
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 deviceId than the previous fix.
fraud_mocked_from_mock_provider
The track had mocked: true.
fraud_inaccurate_exceeded_accuracy_threshold
accuracy worse 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.
user.fraud after a jump with a mock provider
{
  "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
}
Fraud
FieldTypeDescription
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.

Body
NameTypeDescription
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.
Request
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" }] }'
Response · 200
{
  "blocked": 1
}

POST /v1/fraud/unblock

Secret key only

Lift blocks.

Body
NameTypeDescription
items required array 1–500 of { userId }. unblocked counts the users that exist.
Request
curl -X POST "https://api.mapsocket.com/v1/fraud/unblock" \
  -H "Authorization: sk_live_…" -H "Content-Type: application/json" \
  -d '{ "items": [{ "userId": "driver-118" }] }'
Response · 200
{
  "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.

Request headers
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
Request body
{
  "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:

Header
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 2xx response 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 createdAt if order matters.

Verify in Node

server.js (Express)
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);