# Fencemaker > Geofencing-as-a-Service REST API for territory assignment, point-in-polygon checks, real-time device tracking, and entry/exit webhooks. No GIS expertise required. Base URL: https://fencemaker.app ## Docs - [API Reference & Guides](https://fencemaker.app/docs): Full documentation — authentication, webhooks, alerts, and all API endpoints - [Quickstart Guides](https://fencemaker.app/quickstart): Use-case walkthroughs (delivery zones, territory assignment, dynamic pricing, real-time alerts, route compliance, idle time detection) - [API Keys](https://fencemaker.app/keys): Generate and manage API keys ## Authentication All `/api/v1/*` endpoints require the `X-API-Key` header. ``` X-API-Key: gfnsr_live_<24-byte-base64url> ``` Keys are SHA-256 hashed at rest and never retrievable after creation. Scope is per-organisation — all queries are auto-scoped to the org that owns the key. **Errors** - `401` — `{ "error": "Invalid or missing API key" }` - `400` — `{ "error": " is required" }` - `404` — `{ "error": "Resource not found" }` - `429` — `{ "error": "Rate limit exceeded. Retry after 60s.", "retry_after": 60 }` ## Core Concepts **Territory** — A named GeoJSON Polygon geofence with optional `code`, `agent_id`, `color`, and per-zone `webhook_url`. Inactive territories are excluded from PIP checks and event tracking. **Device** — A tracked object (rider, vehicle, asset) identified by a string `device_id`. Auto-created on first `/track` ping; optionally pre-registered with a label. **Event** — An `entered` or `exited` record written every time a device crosses a territory boundary. Always persisted regardless of webhook success. **Location (Hub)** — Organisational grouping that scopes territories. Filter PIP calls with `location_id` when you have multiple regions. ## Point-in-Polygon **Quick check (GET)** ``` GET /api/v1/pip?lat=12.9716&lon=77.5946&location_id=&multi=true ``` Returns `{ matched, territory: { id, name, code, agent_id, location_id, location_name }, lat, lon, response_ms }`. Add `multi=true` to return all matching territories instead of the first. **Full check with address geocoding (POST)** ``` POST /api/v1/territory { "lat": 12.9716, "lon": 77.5946, "location_id": "uuid" } // or { "address": "14 MG Road, Bengaluru" } ``` Returns territory + agent details (`territory_id`, `territory_code`, `agent_id`, `agent_name`, `location_name`). **Batch (up to 1,000 points)** ``` POST /api/v1/pip/batch { "location_id": "uuid", "points": [ { "id": "order-1", "lat": 12.97, "lon": 77.59 }, { "id": "order-2", "address": "14 MG Road Bengaluru" } ] } ``` Returns `{ total, matched, unmatched, match_rate, results: [{ id, matched, territory_id, territory_code, agent_id }] }`. ## Territories ``` GET /api/v1/territories?location_id=&page=1&per_page=50 GET /api/v1/territories/:id POST /api/v1/territories { "name": "Warehouse A", "boundary": { "type": "Polygon", "coordinates": [[...]] }, "hub_id": "uuid", "code": "WH-A", "color": "#ff5500" } PATCH /api/v1/territories/:id { "active": false } — deactivate/reactivate a geofence ``` Territory response includes: `id`, `name`, `code`, `agent_id`, `agent_name`, `location_id`, `location_name`, `color`, `webhook_url`, `geometry` (GeoJSON Polygon). Plan limit error: `403` — `{ "error": "Territory limit reached (25/25)", "upgrade_url": "https://fencemaker.app/pricing" }`. ## Devices & Tracking **Register (optional — devices auto-create on first ping)** ``` POST /api/v1/devices { "device_id": "RIDER-042", "label": "Rahul's bike" } ``` **List / status** ``` GET /api/v1/devices?device_id=RIDER-042&page=1&per_page=50 GET /api/v1/devices/:id/status ``` Status returns `{ device_id, label, last_lat, last_lon, last_seen, territories_inside: [{ id, code, name, agent_id }] }`. **GPS ping (triggers geofence evaluation + webhooks)** ``` POST /api/v1/track { "device_id": "RIDER-042", "lat": 12.9716, "lon": 77.5946 } ``` Returns `{ device_id, events: [{ type, territory_code, webhook_fired }], territories_inside: ["uuid"], response_ms }`. **Batch pings (up to 100 devices)** ``` POST /api/v1/track/batch { "pings": [{ "device_id": "RIDER-042", "lat": 12.97, "lon": 77.59 }, ...] } ``` ## Events & Webhooks **Query event history** ``` GET /api/v1/events?device_id=RIDER-042&territory_id=&event_type=entered&since=2026-01-01&until=2026-06-01&limit=100&page=1 ``` **Simulate an event (for webhook testing)** ``` POST /api/v1/events/simulate { "territory_id": "uuid", "device_id": "TEST-01", "event_type": "entered" } ``` **Webhook delivery health** ``` GET /api/v1/webhooks/health → { total, delivered, failed, delivery_rate, avg_response_ms } ``` **Webhook payload (per-territory webhook, set in Territory Editor)** ```json { "event": "entered", "device_id": "RIDER-042", "territory_id": "uuid", "territory_code": "ZONE-A", "territory_name": "South Bengaluru", "lat": 12.9716, "lon": 77.5946, "timestamp": "2026-03-06T10:45:00.000Z", "org_id": "your-org-uuid" } ``` Exit events additionally include `dwell_seconds` (time spent inside the zone). **Custom Webhook channel payload (org-wide, supports HMAC signing)** ```json { "event": "geofence.entered", "timestamp": "2026-04-28T10:45:00.000Z", "data": { "device_id": "RIDER-042", "territory_id": "uuid", "territory_name": "South Bengaluru", "event_type": "entered", "location": { "latitude": 12.9716, "longitude": 77.5946 } } } ``` Signing header: `X-Fencemaker-Signature: sha256=` (HMAC-SHA256 over raw body). Verify with `crypto.timingSafeEqual`. Delivery: POST, 10s timeout, 3 retries on 5xx (30s / 60s / 120s backoff). 4xx responses are not retried. ## Alerts (Slack / Telegram / Custom Webhook) Configure notification channels in the Alerts & Webhooks dashboard. All channels fire org-wide on every `entered` / `exited` event simultaneously. - **Slack** — paste an Incoming Webhook URL; receive formatted Block Kit messages with device, zone, timestamp, and map link. - **Telegram** — connect via one-time token; bot commands: `/connect `, `/status`. - **Custom Webhook** — any HTTPS endpoint; optional HMAC-SHA256 signing secret. ## Geocoding ``` GET /api/v1/geocode/forward?address=14+MG+Road+Bengaluru → { lat, lon, formatted, response_ms } GET /api/v1/geocode/reverse?lat=12.9716&lon=77.5946 → { formatted, city, state, country, response_ms } GET /api/v1/search/autocomplete?q=mg+road&near=12.97,77.59 → { results: [{ label, lat, lon }] } ``` ## Analytics ``` GET /api/v1/overlaps?location_id=&refresh=true → { overlaps: [{ territory_a, territory_b, name_a, name_b, overlap_area_sqkm }], count } GET /api/v1/usage?period=7d&endpoint=/api/v1/pip → { period, total_requests, by_endpoint: [{ endpoint, count }] } ``` ## Map Embed **Vector tiles (MapLibre-compatible PBF)** ``` GET /api/v1/map/tiles/{z}/{x}/{y} Content-Type: application/x-protobuf Cache-Control: public, max-age=86400 ``` Use as `tiles` URL in a MapLibre vector source. Attach key via `transformRequest`. **Territories as GeoJSON** ``` GET /api/v1/map/territories?bbox=minLon,minLat,maxLon,maxLat&code=ZONE-A,ZONE-B → GeoJSON FeatureCollection — each feature has id, name, code, agent_id, color, location_id as properties ``` ## Optional - [llms.txt spec](https://llmstxt.org): This file follows the llms.txt standard for AI-readable documentation