Quick Start
Get from zero to your first geofence check in 5 minutes.
Get Your API Key
Go to API Keys and create a new key. The full key is shown once — copy it immediately.
gfnsr_live_aBcDeFgHiJkLmNoPqRsT...
Your First PiP Call
Check which territory a coordinate belongs to:
curl -X GET "https://fencemaker.app/api/v1/pip?lat=12.9716&lon=77.5946" \ -H "X-API-Key: YOUR_API_KEY"
{
"matched": true,
"territory": { "name": "South Bengaluru", "code": "ZONE-A", "agent_id": "R-001" },
"response_ms": 8
}Register a Device (optional)
Device registration is optional. The /track endpoint auto-creates devices on first GPS ping. Use this only if you want to set labels or metadata upfront:
curl -X POST https://fencemaker.app/api/v1/devices \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"device_id": "RIDER-042", "label": "Rahul's bike"}'Send a GPS Ping
Forward device location to evaluate geofences:
curl -X POST https://fencemaker.app/api/v1/track \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"device_id": "RIDER-042", "lat": 12.9716, "lon": 77.5946}' {
"events": [{ "type": "entered", "territory_code": "ZONE-A", "webhook_fired": true }],
"response_ms": 12
}Test Your Webhook
Simulate an event without a real device:
curl -X POST https://fencemaker.app/api/v1/events/simulate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"territory_id": "UUID", "device_id": "TEST-01", "event_type": "entered"}'POST /api/v1/pip/batch to check up to 1,000 points in one request.GET /api/v1/map/tiles/{z}/{x}/{y} as a MapLibre tile source and GET /api/v1/map/territories for your zone polygons as GeoJSON — one API key, no third-party map dependency. See the How It Works guide for full integration code.Code Examples
Python
import requests
API_KEY = "gfnsr_live_..."
BASE = "https://fencemaker.app"
# Single PiP check
r = requests.get(f"{BASE}/api/v1/pip", params={"lat": 12.97, "lon": 77.59},
headers={"X-API-Key": API_KEY})
print(r.json())
# Track a device
r = requests.post(f"{BASE}/api/v1/track",
json={"device_id": "RIDER-042", "lat": 12.97, "lon": 77.59},
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"})
print(r.json()["events"]) Node.js
const API_KEY = "gfnsr_live_...";
const BASE = "https://fencemaker.app";
// PiP check
const res = await fetch(`${BASE}/api/v1/pip?lat=12.97&lon=77.59`, {
headers: { "X-API-Key": API_KEY }
});
console.log(await res.json());
// Track
const t = await fetch(`${BASE}/api/v1/track`, {
method: "POST",
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ device_id: "RIDER-042", lat: 12.97, lon: 77.59 })
});
console.log((await t.json()).events);Authentication
All API v1 endpoints require an API key.
Getting Your Key
- Go to API Keys
- Click Generate New Key
- Name it (e.g. "Production Backend")
- Copy immediately — shown only once
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
| Prefix | gfnsr_live_ |
| Random | 24 bytes, base64url encoded |
| Storage | SHA-256 hash only |
Errors
{ "error": "Invalid or missing API key" } Best Practices
- Environment variables — never hardcode keys
- One key per environment — dev, staging, production
- Server-side only — never expose in frontend code
- 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.
How It Works
POST /api/v1/trackentered / exited eventSetup
Set a webhook URL on your territory in the Territory Editor.
Payload
{
"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"
} Headers
| Content-Type | application/json |
| X-Fencemaker-Event | entered or exited |
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" }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/track{ "device_id": "RIDER-042", "lat": 12.9716, "lon": 77.5946 } {
"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 must be between -90 and 90" }/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 }
]
} {
"processed": 2,
"total_events": 1,
"events": [
{
"device_id": "RIDER-042",
"type": "entered",
"territory_code": "ZONE-A",
"webhook_fired": true
}
],
"response_ms": 28
} 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 }
}