Documentation
Quick Start
Pick a use case and get a complete integration guide with real endpoints and copy-paste code.
Authentication
All API v1 endpoints require an API key.
Getting Your Key
- Go to Alerts → API Keys
- Choose a key type (see below) and click Create Key
- Name it (e.g. "Production Backend")
- Copy immediately — shown only once
Key Types
Two key types, both created from the same Alerts page section:
Secret (gfnsr_live_...) | Full API access. Server-side only — never embed in an app or website. |
Publishable (gfnsr_pub_...) | Limited to POST /api/v1/track, POST /api/v1/track/batch, and GET /api/v1/map/territories (used by the mobile SDK's local polygon cache — see the mobile integration guide). Can send device locations and read zone boundaries. If it leaks, someone could send fake locations or see your zone shapes, so revoke it and issue a new one. |
Both count against the same 10-key-per-plan limit.
Using the Key
Include it in the X-API-Key header:
curl -H "X-API-Key: gfnsr_live_..." https://fencemaker.app/api/v1/pip?lat=12.97&lon=77.59
Key Format
| Secret prefix | gfnsr_live_ |
| Publishable prefix | gfnsr_pub_ |
| Random | 24 bytes, base64url encoded |
| Storage | SHA-256 hash only |
Errors
{ "error": "Invalid or missing API key" } { "error": "Publishable keys are restricted to /api/v1/track, /api/v1/track/batch, and GET /api/v1/map/territories.", "hint": "Use a secret key (gfnsr_live_...) for this endpoint." } Best Practices
- Use a publishable key for anything client-side — mobile apps, embedded SDKs — never a secret key
- Environment variables — never hardcode secret keys
- One key per environment — dev, staging, production
- Rotate periodically — revoke old, create new
- Monitor usage — check Usage for anomalies
Organisation Scoping
Each key belongs to an organisation. All queries auto-scope to your org's data. No cross-org data access is possible.
Webhook Integration
Receive real-time notifications when devices enter or exit geofenced territories. Prefer a no-code destination? See the Slack and Telegram integrations — both are free and built on this same webhook layer. Full walkthrough: Custom Webhooks integration guide.
How It Works
POST /api/v1/trackentered / exited eventSetup
Set a webhook URL on your territory in the Territory Editor.
Payload
Entry event:
{
"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",
"event_id": "uuid"
} Exit event — includes dwell_seconds (time spent inside the zone):
{
"event": "exited",
"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:59:07.000Z",
"org_id": "your-org-uuid",
"dwell_seconds": 847,
"event_id": "uuid"
} event_id uniquely identifies this delivery attempt's underlying geofence event — use it to de-duplicate if your endpoint ever receives the same delivery twice.
Headers
| Content-Type | application/json |
| X-Fencemaker-Event | entered or exited |
| X-Fencemaker-Event-Id | Same UUID as the event_id body field — check this header if you dedupe before parsing the body. |
Signature Verification
The Custom Webhook channel (configured in Alerts & Webhooks) supports HMAC-SHA256 signing. Add a signing secret in the dashboard and verify the signature in your handler:
// Header sent with every delivery (when a secret is set):
X-Fencemaker-Signature: sha256=<hex-digest>
// Node.js verification
const crypto = require("crypto");
function verify(secret, rawBody, sigHeader) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader));
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-fencemaker-signature"];
if (!verify(process.env.WEBHOOK_SECRET, req.body, sig))
return res.status(401).json({ error: "Invalid signature" });
const event = JSON.parse(req.body);
// handle event…
res.json({ ok: true });
}); Events
entered | Device moved into a territory |
exited | Device moved out of a territory |
Your Handler
Python (Flask)
@app.post("/webhooks/fencemaker")
def handle(request):
event = request.json
if event["event"] == "entered":
notify(f'{event["device_id"]} arrived at {event["territory_name"]}')
return {"ok": True}, 200 Node.js (Express)
app.post("/webhooks/fencemaker", (req, res) => {
const { event, device_id, territory_name } = req.body;
if (event === "entered") sendAlert(`${device_id} at ${territory_name}`);
res.json({ ok: true });
}); Testing
curl -X POST https://fencemaker.app/api/v1/events/simulate \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"territory_id": "UUID", "device_id": "TEST-01", "event_type": "entered"}' Delivery
| Timeout | 10 seconds |
| Method | HTTP POST |
| Retries | 3 retries on 5xx (30s, 60s, 120s backoff). 4xx responses are not retried. |
| Logging | All attempts logged — visible in Webhook Logs |
geofence_events regardless of webhook success. Query GET /api/v1/events to catch up.Alerts & Notifications
Receive instant alerts in Slack, Telegram, or via a signed webhook whenever a geofence event fires. All channels are configured in Alerts & Webhooks and fire simultaneously on every entered / exited event.
Slack
Setup
- Go to api.slack.com/messaging/webhooks → Create an app → enable Incoming Webhooks → add to a channel → copy the webhook URL.
- In Alerts & Webhooks, click Connect Slack, paste the URL, and click Connect.
- Click Send Test to confirm delivery.
What you receive
A formatted Slack Block Kit message with event type, device ID, zone name, timestamp, and a Google Maps link for the coordinates.
🟢 Geofence ENTERED Device: RIDER-042 Zone: South Bengaluru Time: Today at 10:45 AM Location: View on Map →
Telegram
Setup
- In Alerts & Webhooks, click Connect Telegram — Fencemaker generates a one-time token (expires in 10 minutes).
- Link your chat using either method:
- Mobile (one tap): tap Open in Telegram — the bot connects automatically.
- Desktop / manual: open Telegram, search
@{TELEGRAM_BOT_NAME}, send/connect <token>.
- Click ↻ I've connected — Refresh to confirm, then Send Test to verify.
Bot commands
/start | Setup instructions |
/connect <token> | Link to a Fencemaker account |
/status | Check which org this chat is connected to |
What you receive
🟢 Geofence ENTERED 📱 Device: RIDER-042 📍 Zone: South Bengaluru ⏰ Time: 28 Apr 2026, 10:45:00 🗺 Location: View on Map
Custom Webhook
Setup
- In Alerts & Webhooks, enter your endpoint URL in the Custom Webhook card and click Save URL.
- Optionally click + Signing Secret to add an HMAC key (see Signature Verification below).
- Click Send Test to fire a sample payload.
Payload
Fencemaker POSTs JSON to your endpoint on every geofence event:
POST https://your-server.com/webhook
Content-Type: application/json
User-Agent: Fencemaker-Webhook/1.0
X-Fencemaker-Signature: sha256=<hex> // only if signing secret is set
{
"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 }
}
} Event values
geofence.entered | Device moved into a territory |
geofence.exited | Device moved out of a territory |
Signature Verification
When a signing secret is configured, every request carries X-Fencemaker-Signature: sha256=<hmac-hex> computed over the raw JSON body.
// Node.js (Express)
const crypto = require("crypto");
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-fencemaker-signature"];
const hmac = "sha256=" + crypto.createHmac("sha256", process.env.SECRET)
.update(req.body).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(sig)))
return res.status(401).json({ error: "Invalid signature" });
const { event, data } = JSON.parse(req.body);
console.log(event, data.device_id, data.territory_name);
res.json({ ok: true });
}); # Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhook")
def webhook():
sig = request.headers.get("X-Fencemaker-Signature", "")
expected = "sha256=" + hmac.new(
os.environ["SECRET"].encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
payload = request.json
print(payload["event"], payload["data"]["device_id"])
return {"ok": True} Delivery
| Method | HTTP POST |
| Timeout | 5 seconds |
| Retries | None — use Webhook Logs to detect failures and replay via POST /api/v1/events/simulate |
{ ok: true } immediately and process asynchronously if your handler is slow.API Reference
Base URL: https://fencemaker.app — all endpoints require X-API-Key.
// 401 — invalid or missing API key
{ "error": "Invalid or missing API key" }
// 400 — bad request (missing field, invalid value)
{ "error": "lat and lon are required" }
// 404 — resource not found
{ "error": "Territory not found" }
// 429 — rate limit exceeded
{ "error": "Rate limit exceeded. Retry after 60s.", "retry_after": 60 }Point-in-Polygon
/api/v1/pip// matched: true
{
"matched": true,
"territory": {
"id": "uuid",
"name": "South Bengaluru",
"code": "ZONE-A",
"agent_id": "R-001",
"location_id": "uuid",
"location_name": "Bengaluru"
},
"lat": 12.9716, "lon": 77.5946,
"response_ms": 8
}
// matched: false (coordinate outside all territories)
{
"matched": false,
"territory": null,
"lat": 28.6139, "lon": 77.2090,
"response_ms": 6
} // 400 — missing or invalid coordinates
{ "error": "lat and lon are required" }
{ "error": "lat must be between -90 and 90" }
// 401 — invalid or missing API key
{ "error": "Invalid or missing API key" }/api/v1/territory{ "lat": 12.9716, "lon": 77.5946, "location_id": "uuid" }
// OR with address:
{ "address": "14 MG Road, Bengaluru" } // matched: true
{
"matched": true,
"territory_id": "uuid",
"territory_code": "ZONE-A",
"agent_id": "R-001",
"agent_name": "Rahul S.",
"location_name": "Bengaluru South",
"response_ms": 8
}
// matched: false
{
"matched": false,
"territory_id": null,
"response_ms": 7
} /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" }
]
} {
"total": 2,
"matched": 1,
"unmatched": 1,
"match_rate": 50,
"results": [
{
"id": "order-1",
"matched": true,
"territory_id": "uuid",
"territory_code": "ZONE-A",
"territory_name": "South Bengaluru",
"agent_id": "R-001"
},
{
"id": "order-2",
"matched": false,
"territory_id": null,
"territory_code": null
}
],
"response_ms": 45
} Territories
/api/v1/territories{
"territories": [
{
"id": "uuid",
"name": "South Bengaluru",
"code": "ZONE-A",
"agent_id": "R-001",
"agent_name": "Rahul S.",
"location_id": "uuid",
"location_name": "Bengaluru South",
"color": "#3B82F6"
}
],
"total": 42,
"page": 1,
"per_page": 50
} /api/v1/territories/:id{
"id": "uuid",
"name": "South Bengaluru",
"code": "ZONE-A",
"agent_id": "R-001",
"agent_name": "Rahul S.",
"location_id": "uuid",
"location_name": "Bengaluru South",
"color": "#3B82F6",
"webhook_url": "https://example.com/hook",
"geometry": { "type": "Polygon", "coordinates": [[...]] }
} // 404 — territory not found
{ "error": "Territory not found" }/api/v1/territories{
"name": "Warehouse Zone A",
"boundary": { "type": "Polygon", "coordinates": [[...]] },
"hub_id": "uuid",
"code": "WH-A",
"color": "#ff5500"
} {
"id": "uuid",
"name": "Warehouse Zone A",
"code": "WH-A",
"color": "#ff5500",
"location_id": "uuid",
"created_at": "2026-05-06T10:00:00Z"
} // 400 — missing required field
{ "error": "name and boundary are required" }
// 403 — plan territory limit reached
{ "error": "Territory limit reached (25/25)", "upgrade_url": "https://fencemaker.app/pricing" }/api/v1/territories/:id{ "active": false } {
"id": "uuid",
"name": "Warehouse Zone A",
"code": "WH-A",
"is_active": false,
"updated_at": "2026-05-06T10:05:00Z",
"response_ms": 12
} // 400 — invalid body
{ "error": "active must be a boolean" }
// 404 — territory not found
{ "error": "Territory not found" }/api/v1/territories/corridor{
"name": "NH48 Highway Corridor",
"code": "CORRIDOR-01",
"hub_id": "uuid",
"buffer_meters": 50,
"paths": [
[
{ "lat": 12.9716, "lon": 77.5946 },
{ "lat": 12.9800, "lon": 77.6010 },
{ "lat": 12.9850, "lon": 77.6100 }
]
],
"webhook_url": "https://example.com/hook",
"color": "#F59E0B"
} {
"id": "uuid",
"name": "NH48 Highway Corridor",
"code": "CORRIDOR-01",
"geometry_type": "corridor",
"buffer_meters": 50,
"path_count": 1,
"total_waypoints": 3,
"geometry": { "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [[...]] } },
"color": "#F59E0B",
"webhook_url": "https://example.com/hook",
"response_ms": 14
} // 400 — missing or invalid fields
{ "error": "name is required" }
{ "error": "paths is required and must be an array" }
{ "error": "buffer_meters is required and must be a number" }
{ "error": "hub_id (or location_id) is required" }
// 400 — invalid path data
{ "error": "At least one path is required" }
{ "error": "Maximum 5 paths per corridor" }
{ "error": "Path 1 must have at least 2 waypoints" }
{ "error": "Path 1 exceeds maximum of 500 waypoints" }
{ "error": "buffer_meters must be between 1 and 10000" }
// 404 — hub not found for this org
{ "error": "hub_id not found" }
// 403 — plan territory limit reached
{ "error": "Territory limit reached (25/25)", "upgrade_url": "https://fencemaker.app/pricing" }Hubs
/api/v1/hubs{
"hubs": [
{ "id": "uuid", "name": "Default" }
],
"total": 1,
"page": 1,
"per_page": 50
} Devices & Tracking
/api/v1/devices{ "device_id": "RIDER-042", "label": "Rahul's bike" } { "id": "uuid", "device_id": "RIDER-042", "auto_created": false, "response_ms": 3 } /api/v1/devices{
"devices": [
{
"id": "uuid",
"device_id": "RIDER-042",
"label": "Rahul's bike",
"last_lat": 12.9716,
"last_lon": 77.5946,
"last_seen": "2026-03-06T10:45:00.000Z",
"territories_inside": ["uuid"]
}
],
"total": 1,
"page": 1,
"per_page": 50
} /api/v1/devices/:id/status{
"device_id": "RIDER-042",
"label": "Rahul's bike",
"last_lat": 12.9716,
"last_lon": 77.5946,
"last_seen": "2026-03-06T10:45:00.000Z",
"territories_inside": [
{
"id": "uuid",
"code": "ZONE-A",
"name": "South Bengaluru",
"agent_id": "R-001"
}
]
} // 404 — device not found
{ "error": "Device not found" }/api/v1/devices/:id{
"label": "Rahul's bike",
"device_group": "bengaluru-south",
"tags": ["VIP", "fragile"],
"status": "delayed"
} {
"id": "uuid",
"device_id": "RIDER-042",
"label": "Rahul's bike",
"device_group": "bengaluru-south",
"tags": ["VIP", "fragile"],
"status": "delayed",
"last_lat": 12.9716,
"last_lon": 77.5946,
"last_seen_at": "2026-03-06T10:45:00.000Z",
"is_active": true,
"response_ms": 4
} // 400 — invalid status
{ "error": "status must be one of: active, delayed, needs_review" }
// 400 — invalid tags
{ "error": "tags must be an array of strings" }
{ "error": "tag \"...\" exceeds 40 characters" }
{ "error": "tags: maximum 20 tags allowed" }
// 400 — no allow-listed fields in body
{ "error": "Nothing to update" }
// 404 — device not found, or belongs to another org (identical response either way)
{ "error": "Device not found" }/api/v1/track{
"device_id": "RIDER-042",
"lat": 12.9716,
"lon": 77.5946,
"accuracy": 8, // optional, GPS accuracy in metres, >= 0
"speed": 32, // optional, km/h, 0-400
"battery": 71, // optional, percent, 0-100
"heading": 90, // optional, degrees, 0-360
"timestamp": "2026-06-17T10:00:00Z" // optional, ISO 8601, defaults to now
} {
"device_id": "RIDER-042",
"events": [
{ "type": "entered", "territory_code": "ZONE-A", "webhook_fired": true }
],
"territories_inside": ["uuid"],
"response_ms": 12
} // 400 — invalid or missing fields
{ "error": "device_id is required" }
{ "error": "lat and lon are required" }
{ "error": "speed must be between 0 and 400 km/h" }
{ "error": "battery must be between 0 and 100" }
{ "error": "heading must be between 0 and 360" }
{ "error": "accuracy must be >= 0" }
// 400 — timestamp out of bounds
{ "error": "timestamp cannot be more than 5 minutes in the future." }
{ "error": "timestamp too far in the past (max 24h)." }
// 503 — event write failed, state was not advanced; safe to retry
{ "error": "Failed to record event, please retry." }
// Retry-After: 2 (header, seconds) — wait this long before retrying
// 429 — this API key has hit its daily new-device creation cap (only for a device_id never seen before; existing devices are never affected)
{ "error": "Daily new-device limit reached for this API key (20/day). Use an existing device_id, or pre-register devices instead of relying on auto-create." }
// Retry-After header gives seconds until the window resets
// 403 — creating this device would exceed your plan's device limit (only for a new device_id; existing devices are never affected)
{ "error": "Device limit reached for your plan (free)", "limit": 5, "current": 5, "upgrade_url": "https://fencemaker.app/pricing" }/api/v1/track/batch{
"pings": [
{ "device_id": "RIDER-042", "lat": 12.97, "lon": 77.59 },
{ "device_id": "RIDER-043", "lat": 12.96, "lon": 77.60 }
]
} {
"total": 2,
"events_fired": 1,
"results": [
{
"device_id": "RIDER-042",
"events": [
{ "type": "entered", "territory_code": "ZONE-A", "webhook_fired": true }
],
"territories_inside": ["uuid"]
},
{
"device_id": "RIDER-043",
"error": "Failed to record event, please retry.",
"retryable": true
}
],
"response_ms": 28
} Trips
/api/v1/trips{
"device_id": "uuid",
"stops": [
{ "sequence": 1, "type": "pickup", "lat": 12.97, "lon": 77.59 },
{ "sequence": 2, "type": "delivery", "lat": 12.95, "lon": 77.62, "planned_eta": "2026-03-06T14:00:00.000Z" }
]
} {
"id": "uuid",
"device_id": "uuid",
"status": "planned",
"stops": [
{ "id": "uuid", "sequence": 1, "type": "pickup", "lat": 12.97, "lon": 77.59, "territory_id": "uuid" },
{ "id": "uuid", "sequence": 2, "type": "delivery", "lat": 12.95, "lon": 77.62, "territory_id": "uuid" }
],
"response_ms": 40
} /api/v1/trips{ "total": 1, "page": 1, "per_page": 50, "trips": [ { "id": "uuid", "status": "in_progress", "device_id": "uuid", "created_at": "..." } ] } /api/v1/trips/:id{
"id": "uuid",
"status": "in_progress",
"stops": [
{ "id": "uuid", "sequence": 1, "arrived_at": "...", "departed_at": "...", "dwell_seconds": 312 },
{ "id": "uuid", "sequence": 2, "arrived_at": null, "departed_at": null }
],
"current_stop": null,
"next_stop": { "id": "uuid", "sequence": 2 },
"deviated": false,
"delayed": true,
"distance_remaining_m": 5432,
"duration_remaining_seconds": 612,
"eta": "2026-03-06T14:12:00.000Z"
} /api/v1/trips/:id/start{ "id": "uuid", "status": "in_progress", "actual_start": "..." } /api/v1/trips/:id/complete{ "id": "uuid", "status": "completed", "actual_end": "..." } Events & Webhooks
/api/v1/events{
"events": [
{
"id": "uuid",
"device_id": "RIDER-042",
"territory_id": "uuid",
"territory_code": "ZONE-A",
"event_type": "entered",
"lat": 12.9716,
"lon": 77.5946,
"created_at": "2026-03-06T10:45:00.000Z"
}
],
"total": 1,
"page": 1
} /api/v1/events/simulate{ "territory_id": "uuid", "device_id": "TEST-01", "event_type": "entered" } { "simulated": true, "webhook_fired": true, "response_ms": 5 } /api/v1/webhooks/health{
"total": 120,
"delivered": 117,
"failed": 3,
"delivery_rate": 97.5,
"avg_response_ms": 210
} Geocoding
/api/v1/geocode/forward{
"lat": 12.9716,
"lon": 77.5946,
"formatted": "14 MG Road, Bengaluru, Karnataka 560001, India",
"response_ms": 90
} /api/v1/geocode/reverse{
"formatted": "14 MG Road, Bengaluru, Karnataka 560001, India",
"city": "Bengaluru",
"state": "Karnataka",
"country": "India",
"response_ms": 85
} /api/v1/search/autocomplete{
"results": [
{ "label": "MG Road, Bengaluru", "lat": 12.9752, "lon": 77.6074 },
{ "label": "MG Road, Pune", "lat": 18.5204, "lon": 73.8567 }
]
} Analytics
/api/v1/overlaps{
"overlaps": [
{
"territory_a": "uuid",
"territory_b": "uuid",
"name_a": "ZONE-A",
"name_b": "ZONE-B",
"overlap_area_sqkm": 0.42
}
],
"count": 1
} /api/v1/usage{
"period": "7d",
"total_requests": 14230,
"by_endpoint": [
{ "endpoint": "/api/v1/pip", "count": 8100 },
{ "endpoint": "/api/v1/track", "count": 5200 },
{ "endpoint": "/api/v1/pip/batch", "count": 930 }
]
} Map & Embed
/api/v1/map/tiles/{z}/{x}/{y}// Binary PBF tile (application/x-protobuf)
// Cache-Control: public, max-age=86400
// Usage in MapLibre:
const map = new maplibregl.Map({
style: {
sources: { fm: {
type: 'vector',
tiles: ['https://fencemaker.app/api/v1/map/tiles/{z}/{x}/{y}'],
maxzoom: 15
}},
layers: [ /* roads, water, labels … */ ]
},
transformRequest: (url) => ({
url, headers: { 'X-API-Key': YOUR_KEY }
})
}); /api/v1/map/territories{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [[...]] },
"properties": {
"id": "uuid",
"name": "South Bengaluru",
"code": "ZONE-A",
"agent_id": "R-001",
"agent_name": "Rahul S.",
"color": "#3B82F6",
"location_id": "uuid",
"location_name": "Bengaluru South"
}
}
],
"meta": { "count": 1, "generated_at": "2026-03-09T…", "response_ms": 11 }
}