Send webhooks when vehicles enter or leave a zone
Register a geofence once and latlng calls your service every time a vehicle crosses it. You'll build a small receiver, register a depot zone, and see how retries keep events from getting lost.
Beginner · about 15 minutes · tested against latlng v0.2.0
Before you start
-
latlng-serverinstalled, and familiarity with the vehicle tracker tutorial. - Node.js 18+ for the webhook receiver (any HTTP server works).
01 Run latlng with durable storage
Webhook jobs are written to the same append-only log as your data, so pending deliveries survive a restart (see storage and guarantees). Start the server with --aof and create a fleet if you haven't already.
latlng-server --aof ./data/appendonly.aof
curl -s -X POST http://127.0.0.1:7421/collections/fleet-hamburg 02 Write a webhook receiver
latlng sends each geofence event as an HTTP POST with a JSON body. Any 2xx response acknowledges the delivery; anything else, or a timeout, schedules a retry.
Delivery is at-least-once, so the same event can arrive twice — for example, if your service processed it but the response was lost. Every request carries a stable X-LatLng-Event-Id header; store the IDs you've handled and skip repeats.
import { createServer } from "node:http";
const seen = new Set(); // use a database or Redis in production
createServer(async (req, res) => {
let body = "";
for await (const chunk of req) body += chunk;
const eventId = req.headers["x-latlng-event-id"];
if (seen.has(eventId)) {
res.writeHead(200).end(); // duplicate: acknowledge and ignore
return;
}
seen.add(eventId);
const event = JSON.parse(body);
const { lat, lon } = event.object.Point;
console.log(`${event.detect} ${event.id} (${event.hook}) at ${lat}, ${lon}`);
res.writeHead(200).end(); // acknowledge quickly; do slow work asynchronously
}).listen(8080, () => console.log("listening on :8080")); 03 Register a geofence hook
A hook combines a name, an endpoint, and a geofence definition: the collection to watch, the area query, which transitions to detect, and which commands trigger evaluation. This one watches a depot polygon and reports vehicles entering and leaving it.
curl -s -X POST http://127.0.0.1:7421/hooks \
-H 'content-type: application/json' \
-d '{
"name": "depot-altona",
"endpoint": "http://127.0.0.1:8080/geofence",
"def": {
"collection": "fleet-hamburg",
"query": {
"Within": {
"area": { "GeoJson": { "type": "Polygon", "coordinates": [[
[9.930, 53.548], [9.945, 53.548], [9.945, 53.556], [9.930, 53.556], [9.930, 53.548]
]] } },
"options": {}
}
},
"detect": ["Enter", "Exit"],
"commands": ["Set"]
}
}'
# {"ok":true} For a circular zone, use {"Nearby": {"lat": 53.55, "lon": 9.99, "meters": 500, "options": {}}} as the query instead. Other detect types are Inside and Outside (every update inside or outside the zone), Cross (entered and left between two updates), and Roam for proximity between moving objects.
Areas are defined inline — GeoJSON, bounds, or a circle. A reference to a stored object is rejected with 400, and so are out-of-range coordinates or unknown keys in the definition.
To also hear about vehicles that disappear, add Del to commands. Expired objects are deleted by a background sweep and go through the normal delete path, so a vehicle whose TTL runs out inside the zone arrives as "command": "Del" with "detect": "Exit". Use command to tell an expiry or deletion apart from a vehicle that drove out.
04 Drive a vehicle through the zone
Send three position updates: outside, inside, and outside again. Your receiver prints one enter and one exit event.
for pos in "53.5520 9.9500" "53.5520 9.9380" "53.5530 9.9600"; do
lat=${pos% *}; lon=${pos#* }
curl -s -X POST http://127.0.0.1:7421/collections/fleet-hamburg/objects/truck-1 \
-H 'content-type: application/json' \
-d "{\"object\":{\"Point\":{\"lat\":$lat,\"lon\":$lon,\"z\":null}}}"
done Enter truck-1 (depot-altona) at 53.552, 9.938
Exit truck-1 (depot-altona) at 53.553, 9.96 {
"command": "Set",
"detect": "Enter",
"collection": "fleet-hamburg",
"id": "truck-1",
"object": { "Point": { "lat": 53.552, "lon": 9.938, "z": null } },
"fields": { "values": { "speed": { "type": "number", "value": 30.0 } } },
"timestamp_ns": 1790456341202287000,
"event_id": "evt_B1hIgXYGb6W6oBt7yZWiWg",
"job_id": "job_hhZVKdArzJ_MgKMJXxaGcQ",
"hook": "depot-altona",
"group": "depot-altona",
"nearby": null
} 05 See retries in action
Stop the receiver and drive the vehicle into the zone again. The job waits in the queue; start the receiver and it's delivered on the next retry.
curl -s http://127.0.0.1:7421/admin/webhook-queue
# {"dead_letter":0,"leased":0,"oldest_pending_age_ms":0,"pending":1} curl -s http://127.0.0.1:7421/metrics | grep -E '^latlng_hook_'
# latlng_hook_attempts_total every delivery attempt
# latlng_hook_success_total 2xx responses
# latlng_hook_failure_total failed attempts (while the receiver was down)
# latlng_hook_retry_total attempts that scheduled a retry
# latlng_hook_dead_letter_total jobs that exhausted the retry budget Retries back off exponentially from webhook_retry_initial_backoff_ms (200 ms) up to webhook_retry_max_backoff_ms (30 s). After webhook_retry_count retries (8 by default), the job moves to the dead-letter state instead of being dropped, and shows up in the dead_letter count. See delivery settings for every option and operations and observability for the full metric list.
06 Manage hooks
List hooks, read one back with its full definition, and delete it when the zone is no longer needed. Hook definitions are persisted and replayed on restart — see recovery guarantees.
curl -s http://127.0.0.1:7421/hooks
curl -s http://127.0.0.1:7421/hooks/depot-altona
curl -s -X DELETE http://127.0.0.1:7421/hooks/depot-altona Only fire for some vehicles: add filters to the query's options, for example "options": {"where_in_filters": [{"field": "status", "values": ["en-route"]}]} — parked vehicles entering the zone are then ignored.
What you built
- A webhook receiver that acknowledges quickly and ignores duplicate deliveries.
- A polygon geofence that reports vehicles entering and leaving a depot.
- Durable delivery: queued jobs, exponential backoff, and dead-lettering instead of silent loss.
Use channels and the TypeScript SDK to push enter and exit events to a backend service or dashboard in real time.
Use case Delivery zones and arrival notificationsCheck which delivery zone an address falls into, and notify customers and dispatchers the moment a driver arrives at or leaves a stop.
Use case Asset tracking and theft alertsKeep track of trailers, containers, and equipment across sites, raise an alert when an asset leaves its yard, and detect trackers that stop reporting.