Skip to content

Webhooks

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).

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": "…"
}
}
Terminal window
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.

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()
})

Verify against the raw body bytes, before any JSON parsing.

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.

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.

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.