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.