Webhooks
Hear about every order, signed
Add an endpoint with POST /webhooks (or in the console). Each event is a signed POST, retried until your endpoint answers 2xx.
The payload
{
"event": "order.delivered",
"eventId": "cmh…",
"label": "Delivered",
"occurredAt": "2026-11-12T15:42:00.000Z",
"order": { "trackingNumber": "FF26-000412", "status": "DELIVERED", "externalRef": "SO-7700112",
"references": [{ "kind": "PO", "value": "PO-4471" }] },
"stop": { "leg": "DELIVERY", "city": "Springfield", "state": "IL" },
"reason": null
}Events: order.created, order.approved, order.rejected, order.cancelled, order.dispatched, stop.en_route, stop.arrived, order.picked_up, order.delivered, order.completed, order.escalated, exception.raised, batch.completed. Every key is always present (null when empty); fields are only ever added.
Verify the signature
Headers: X-Fleetfully-Event, X-Fleetfully-Delivery (also sent as Idempotency-Key — stable across retries, so drop one you already took) and X-Fleetfully-Signature: t=<unix>,v1=<hex> = HMAC-SHA256(secret, "<t>." + raw body). Compare in constant time and reject a stale t.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (a string), header: X-Fleetfully-Signature
export function verify(secret: string, rawBody: string, header: string, toleranceSec = 300): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return want.length === (parts.v1 ?? "").length && timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1));
}import hashlib, hmac, time
def verify(secret: str, raw_body: bytes, header: str, tolerance_sec: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance_sec:
return False
want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts.get("v1", ""))Retries, the log and replay
Anything but a 2xx is retried with backoff (8 tries); after the last, the carrier sees it. In the console, Settings → Status updates lists every endpoint's deliveries with their answers, sends a failed one again, and replays a range — every event between two times your endpoint never received (nothing already sent goes twice). Sandbox endpoints are under Settings → Developers.
The sandbox inbox
No endpoint yet? POST /inbox with a test key gives your sandbox a URL on our side; point a webhook at it and GET /inbox shows each delivery with whether its signature verified. The quickstart does exactly this.