Fleet sends an HTTPS POST to your endpoint when something you subscribed to happens. Deliveries follow the
Standard Webhooks specification, so any of its libraries can verify them.
Scopes: webhooks:write to manage endpoints, plus the read scope for every event type you subscribe to
(fleet_requests:read, costs:read, accounting:read).
Events
Section titled “Events”| Type | When |
|---|---|
fleet_request.created, .submitted, .returned, .approved, .rejected, .in_service, .service_confirmed, .closed, .cancelled |
A fleet request moved in Fleet. approved is your cue to raise the PR or PO. |
fleet_request.financial_status_changed |
A financial event changed the request’s financial status. |
cost_entry.created, .updated, .voided, .superseded |
The cost ledger changed. Drain the export queue. |
accounting_period.closed |
closed_through moved. |
webhook.test |
Sent only by POST /v1/webhook_endpoints/{id}/test. |
These are the cost and finance events. Every record type (vehicles, fuel logs, expenses, work orders, purchase orders,
parts…) also has created, updated, deleted and, where it has a status, status_changed events: see
all event types. GET /v1/webhook_event_types lists them with the scope each needs. Subscribe with exact types, a prefix such as
cost_entry.*, or *.
Events are thin: they name the object and carry a few fields to route on, not the whole object. Fetch the object for its current state. That way an out-of-order or late delivery can never make you act on stale data.
{ "type": "fleet_request.approved", "timestamp": "2026-11-03T08:14:22.000Z", "data": { "object": "fleet_request", "id": "6f1c2b0e-8a43-4c1e-9d55-0c2b7f3e9a10", "number": "FR-000042", "status": "approved", "financial_status": "not_sent", "vehicle_id": "…" }}Create an endpoint
Section titled “Create an endpoint”curl https://api.get-fleet.com/v1/webhook_endpoints \ -H "Authorization: Bearer $FLEET_API_KEY" -H "Content-Type: application/json" \ -d '{"url": "https://erp.example.com/hooks/fleet", "enabled_events": ["fleet_request.approved", "cost_entry.*"]}'The response contains secret (whsec_…) once. Store it. The URL must be HTTPS on port 443 and resolve to a
public address; Fleet does not follow redirects.
Verify every delivery
Section titled “Verify every delivery”Each request carries webhook-id (the event id, stable across retries), webhook-timestamp (Unix seconds) and
webhook-signature. Reject requests older than five minutes and use the webhook-id to ignore duplicates.
import { Webhook } from 'standardwebhooks' // npm i standardwebhooks
const wh = new Webhook(process.env.FLEET_WEBHOOK_SECRET) // "whsec_…"
app.post('/hooks/fleet', express.raw({ type: 'application/json' }), (req, res) => { let event try { event = wh.verify(req.body, req.headers) // throws on a bad signature or old timestamp } catch { return res.status(400).end() } queue.add(req.headers['webhook-id'], event) // process asynchronously res.status(204).end()})from standardwebhooks.webhooks import Webhook # pip install standardwebhooks
wh = Webhook(os.environ["FLEET_WEBHOOK_SECRET"])
@app.post("/hooks/fleet")def fleet_hook(request): try: event = wh.verify(request.body, dict(request.headers)) except Exception: return Response(status=400) enqueue(request.headers["webhook-id"], event) return Response(status=204)import base64, hashlib, hmac, time
def verify(secret: str, headers, body: bytes) -> bool: msg_id, ts, sigs = headers["webhook-id"], headers["webhook-timestamp"], headers["webhook-signature"] if abs(time.time() - int(ts)) > 300: return False key = base64.b64decode(secret.removeprefix("whsec_")) expected = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()).decode() # The header can hold several signatures ("v1,abc v1,def") while a secret is being rotated. return any(hmac.compare_digest(expected, s.split(",", 1)[1]) for s in sigs.split(" ") if s.startswith("v1,"))Verify against the raw body bytes, before any JSON parsing.
Retries
Section titled “Retries”Answer with any 2xx within 15 seconds; do the work afterwards. Anything else (or a timeout) is retried after 1
minute, 5 minutes, 30 minutes, 2 hours, 5 hours, then every 10 hours, for about three days. An endpoint that keeps
failing for three days is disabled and shows a disabled_reason; fix it, then PATCH it back to
"status": "enabled".
GET /v1/webhook_deliveries?endpoint_id=… shows every attempt’s status code, the start of your response body and
any connection error. POST /v1/webhook_deliveries/{id}/retry sends one again now.
Rotating the secret
Section titled “Rotating the secret”POST /v1/webhook_endpoints/{id}/rotate_secret returns a new secret. For 24 hours by default
(expire_previous_in_hours, up to 168) deliveries are signed with both secrets, so you can deploy the new one
without missing an event.
Catching up without webhooks
Section titled “Catching up without webhooks”GET /v1/events lists the last 30 days of events your key may read, newest first, with type= and
created_after= filters. Use it after downtime, or poll it instead of receiving webhooks.