Webhooks
A webhook subscription sends events to your HTTPS endpoint as they happen: a device registered, a trip completed,
an alert raised, a geofence entered, a stop completed, a fuel purchase flagged, a payment read from your gateway.
The event catalogue lists every event you can subscribe to, with its schema version;
GET /v1/webhooks/events returns the same list with each event's JSON Schema and a sample. Manage subscriptions in
Developer Studio or with the webhooks API
(webhooks:read, webhooks:write).
Subscribe
curl -X POST "https://api.zaitrack.app/v1/webhooks" \
-H "Authorization: Bearer zk_test_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/zaitrack",
"description": "Dispatch board",
"events": ["zaitrack.geofence.*", "zaitrack.alert.raised"],
"filters": {"severityAtLeast": "high"}
}'
- url:
https, at most 2,048 characters, no credentials in it. Private, loopback and link-local addresses are refused at the address each delivery actually connects to, and redirects are not followed. - events: 1 to 50 patterns, each matching at least one event of the catalogue: an exact name
(
zaitrack.alert.raised.v1), a name without its version, a prefix wildcard (zaitrack.alert.*) or*. - filters (optional):
severityAtLeast:info,low,medium,highorcritical; events with a lowerdata.severityare dropped.vehicleIds: up to 500 UUIDs; events about other vehicles are dropped.divisionIds: up to 50 division UUIDs; only events about vehicles of those divisions, or of the divisions below them, are kept. An event that names no vehicle is dropped.predicate: a JSON Logic expression over the envelope (event_name,region,data), at most 4 KiB, with the operatorsvar,==,!=,<,<=,>,>=,in,and,orand!. Example:{"in": [{"var": "data.severity"}, ["high", "critical"]]}.
- retryPolicy (optional):
maxAttempts1 to 24 (default 12),timeoutS1 to 60 (default 10) andbackoffexponential(default),linearorfixed. - secret (optional): 24 to 128 characters. Left out, the service generates one (
whsec_…). The response carries it once.
An organisation keeps at most 50 subscriptions.
A delivery
Each delivery is an HTTP POST with a JSON body:
{
"schema_version": "1.0",
"event_id": "…",
"event_name": "zaitrack.geofence.entered.v1",
"tenant_id": "…",
"region": "gcc",
"occurred_at": "2026-10-28T06:12:04.512Z",
"produced_at": "2026-10-28T06:12:05.031Z",
"trace_id": "…",
"data": { … }
}
The envelope keys are snake_case, unlike the REST API. data is described by the event's JSON Schema, with
times in RFC 3339. A body over 256 KiB is not delivered.
| Header | Value |
|---|---|
X-Zaitrack-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>, two v1 during a secret's rotation: see Verify a delivery |
X-Zaitrack-Event-Id | the event's id; the same on every attempt and replay |
X-Zaitrack-Event-Name | for example zaitrack.alert.raised.v1 |
X-Zaitrack-Delivery-Id | this delivery (one event to one subscription) |
X-Zaitrack-Attempt | 1 for the first attempt, then 2, 3, … |
X-Zaitrack-Subscription-Id, X-Zaitrack-Tenant-Id, X-Zaitrack-Region | where it came from |
X-Zaitrack-Schema-Version | 1.0 |
X-Zaitrack-Replay | true on a replay |
X-Zaitrack-Test | true on a test send |
X-Zaitrack-Trace-Id | the trace, when the event has one |
An alert's events say it in English (title, description) and language-neutral (code, params), to show
in your users' language: see Alert wording.
Answer with any 2xx within the subscription's timeout. Deduplicate on X-Zaitrack-Event-Id: an event can
arrive more than once.
Test a subscription
POST /v1/webhooks/{id}:test sends one delivery to the endpoint now and answers with it and its attempt.
- With no body, it sends
zaitrack.webhook.test.v1, whosedata.messagesays the endpoint is reachable. - With
{"event": "zaitrack.trip.completed.v1"}, it sends that event with the catalogue's sample as itsdata, so you can test the handler for a real event. Any event of the catalogue can be named.
Either way the body carries "test": true and the request X-Zaitrack-Test: true, so your handler can tell a
test from a real event. A test does not
count towards the subscription's failures.
Rotate the secret
POST /v1/webhooks/{id}/secrets:rotate gives the subscription a new secret, shown once in the response. For an
overlap the old secret keeps signing beside the new one: each delivery carries two v1 signatures, so an endpoint
still holding the old secret keeps verifying while you deploy the new one.
curl -X POST "https://api.zaitrack.app/v1/webhooks/$ID/secrets:rotate" \
-H "Authorization: Bearer zk_test_…" \
-H "Content-Type: application/json" \
-d '{"overlapHours": 48}'
overlapHours is 0 to 168 and 24 when left out; 0 stops the old secret at once.
Retries
A delivery that gets no 2xx (an error status, a timeout, a refused connection) is retried. The wait after
attempt n:
| Backoff | Wait |
|---|---|
exponential | 30 s × 2^(n−1): 30 s, 1 min, 2 min, 4 min, … |
linear | n × 5 min |
fixed | 5 min |
No wait exceeds 60 minutes. After maxAttempts the delivery is dead-lettered (status dlq). An endpoint that
answers 410 Gone is dead-lettered at once and its subscription is disabled. After 25 dead letters in a row the
subscription is disabled too (status: disabled, with disabledReason); a disabled subscription receives
nothing until you set it back to active with PATCH /v1/webhooks/{id}. paused stops deliveries on purpose.
Deliveries and replay
GET /v1/webhooks/{id}/deliverieslists a subscription's deliveries, newest first, with their status:queued,sending,retrying,deliveredordlq.GET /v1/webhooks/{id}/deliveries/{deliveryId}gives the payload and every attempt: its status code, latency, error, the request headers sent, your endpoint's response headers and the first 1 KiB of its answer.POST /v1/webhooks/{id}/deliveries/{deliveryId}:replaysends it again now, withX-Zaitrack-Replay: true. A replay does not use up the retry budget of a delivery still being retried, and is refused (409) while the delivery is being sent.
Dead letters
GET /v1/webhooks/dead-letters lists the deliveries that ran out of attempts across every subscription of your
organisation, newest first (limit, cursor). Once your endpoint is fixed, replay them in one call:
curl -X POST "https://api.zaitrack.app/v1/webhooks/dead-letters:replay" \
-H "Authorization: Bearer zk_test_…" \
-H "Content-Type: application/json" \
-d '{"deliveryIds": ["…", "…"]}'
Up to 100 deliveries a call. Each is sent before the answer, which gives, per delivery, the delivery and its
attempt, or the error that stopped it.
Deliveries are kept for 30 days.