Webhooks
Register endpoints, subscribe to events, verify HMAC signatures, and handle retries and replays.
Webhooks push events to your server the moment they happen — a vehicle arrives at a stop, guidance refreshes, a fueling is detected — so you don't poll. Every delivery is signed so you can verify it came from FuelAtlas and wasn't tampered with.
Register an endpoint
POST /v1/webhooks
Authorization: Bearer fa_live_…
Content-Type: application/jsonRequires webhooks:manage.
{
"url": "https://hooks.yourapp.com/fuelatlas",
"event_types": ["guidance.updated", "vehicle.fuel_low", "fueling.matched"],
"description": "Prod dispatch integration"
}The URL must be HTTPS and publicly reachable — plain http and internal/loopback addresses are rejected at creation. event_types must be a subset of the catalog, or the single wildcard ["*"] to receive everything.
The response returns the endpoint plus a signing secret, shown exactly once:
{
"id": "whk_4mZ…",
"url": "https://hooks.yourapp.com/fuelatlas",
"event_types": ["guidance.updated", "vehicle.fuel_low", "fueling.matched"],
"status": "active",
"secret": "whsec_9c…store_this_now"
}Store secret immediately — you need it to verify deliveries, and it is never shown again. If you lose it, rotate it.
The delivery envelope
Each delivery is a POST to your URL with a JSON body and these headers:
POST /fuelatlas HTTP/1.1
Content-Type: application/json
User-Agent: FuelAtlas-Webhooks/1.0
X-FuelAtlas-Event-Id: evt_2s…
X-FuelAtlas-Event-Type: guidance.updated
X-FuelAtlas-Signature: t=1751813045,v1=6f2b…hex{
"id": "evt_2s…",
"type": "guidance.updated",
"api_version": "2026-07-01",
"sandbox": false,
"sequence": 48213,
"occurred_at": "2026-07-06T15:04:05.1234567Z",
"fleet_id": "flt_9Qm…",
"data": { "vehicle_id": "veh_5tR…", "guidance_id": "gd_7hK…" }
}sequence is a monotonically increasing integer per fleet_id — retries and backoff mean deliveries can arrive out of order, so use sequence (together with id for de-duplication) to detect gaps and reconstruct the correct order, rather than relying on receipt order.
Respond with any 2xx within a few seconds to acknowledge. Anything else (or a timeout) is treated as a failure and retried.
Verifying the signature
The X-FuelAtlas-Signature header is t=<unix-timestamp>,v1=<hex>. To verify: recompute HMAC-SHA256(secret, "{t}.{rawBody}") over the raw request body and constant-time-compare it to the v1 value. Bind the timestamp into a tolerance window (e.g. 5 minutes) to reject replays.
During a secret rotation the header may carry multiple v1= values — one per currently-valid secret. Accept the delivery if any of them matches.
import crypto from "node:crypto";
// rawBody: the exact bytes received (do NOT re-serialize the parsed JSON)
export function verify(rawBody: string, header: string, secret: string, toleranceSec = 300): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
// header may list several v1= values during rotation — match any
return header
.split(",")
.filter((p) => p.startsWith("v1="))
.some((p) => {
const sig = p.slice(3);
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
});
}import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance_sec:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
sigs = [p[3:] for p in header.split(",") if p.startswith("v1=")]
return any(hmac.compare_digest(sig, expected) for sig in sigs)using System.Security.Cryptography;
using System.Text;
static bool Verify(string rawBody, string header, string secret, int toleranceSec = 300)
{
var parts = header.Split(',').Select(p => p.Split('=', 2)).ToList();
var t = long.Parse(parts.First(p => p[0] == "t")[1]);
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > toleranceSec) return false;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{t}.{rawBody}"))).ToLowerInvariant();
return parts.Where(p => p[0] == "v1")
.Any(p => CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(p[1]), Encoding.UTF8.GetBytes(expected)));
}Verify against the raw request body, before any JSON parsing re-serializes it. Re-encoding the parsed object will change the bytes and break the signature.
Event catalog
GET /v1/webhooks/event-types returns this list at runtime (any valid key, no scope). Subscribe to ["*"] to receive all of them.
| Event | Fires when |
|---|---|
guidance.updated | The recommended fuel-stop set for a vehicle was created, refreshed, or cleared. |
guidance.stop_approaching | A vehicle came within the proximity radius of a recommended fuel stop. |
vehicle.stop_arrived | GPS detected a vehicle arriving at a stop. |
vehicle.stop_departed | GPS detected a vehicle departing a stop. |
trip.started | A dispatch trip transitioned to active. |
trip.completed | A dispatch trip completed. |
trip.cancelled | A dispatch trip was cancelled. |
fueling.recorded | A fueling was detected from a tank-level rise. |
fueling.matched | A detected fueling was matched to a recommended or proposed stop. |
vehicle.fuel_low | A vehicle's fuel percentage crossed the low-fuel threshold (with hysteresis). |
vehicle.offline | A previously-reporting vehicle stopped sending location updates. |
vehicle.online | A vehicle that had gone offline resumed reporting. |
fleet.provisioned | A fleet was created via the provisioning API. |
Sandbox endpoints (registered with a fa_test_ key) only receive events flagged sandbox: true; production endpoints only receive live events. The two never cross.
Retries and replays
A delivery that doesn't get a 2xx is retried on a backoff schedule over roughly a day. Deliveries are logged; inspect them with:
curl "https://api.fuelatlas.com/v1/webhooks/whk_4mZ…/deliveries?status=failed" \
-H "Authorization: Bearer fa_live_…"To re-send a specific past event (for example after fixing a bug on your side), replay it:
curl -X POST \
https://api.fuelatlas.com/v1/webhooks/whk_4mZ…/deliveries/whd_8p…/replay \
-H "Authorization: Bearer fa_live_…"A replay re-delivers the same event (marked is_replay: true); it returns 410 event_expired if the event is past its replay window. Design your handler to be idempotent on X-FuelAtlas-Event-Id — replays and rare duplicate deliveries both repeat that id.
Test a delivery
Send a canned sample payload for any event type to your endpoint, synchronously, without waiting for the real event. A test send is signed like any delivery and never disables the endpoint:
curl -X POST https://api.fuelatlas.com/v1/webhooks/whk_4mZ…/test \
-H "Authorization: Bearer fa_live_…" \
-H "Content-Type: application/json" \
-d '{ "event_type": "guidance.updated" }'Rotating the secret
curl -X POST https://api.fuelatlas.com/v1/webhooks/whk_4mZ…/rotate-secret \
-H "Authorization: Bearer fa_live_…"Returns a new secret once. The previous secret stays valid for 24 hours (deliveries in that window are signed with both, so a verifier matching either accepts them). Roll the new secret into your config before the window closes.