The contract in one paragraph
You register a subscription (a URL + a list of event types you care about) via POST /api/v1/webhooks. The response includes a signing secret returned once — store it. When a matching event fires, we POST a JSON envelope to your URL with three headers: X-Fleetgo-Signature (HMAC-SHA256 over ${timestamp}.${body}), X-Fleetgo-Timestamp (unix seconds), and X-Fleetgo-Event (the event type, for routing).
Receivers verify against the raw body (not the parsed JSON — re-serializing changes whitespace and breaks the MAC). Reject deliveries older than 5 minutes (replay window). Persist the envelope's id as an idempotency key in the same transaction as your side effect, so retries don't double-fire.
AsyncAPI spec
Generate typed handlers from /asyncapi.yaml
The full event surface — every channel, every payload shape — is documented in AsyncAPI 2.6 at /asyncapi.yaml. Same role for the push side as /openapi.yaml has for the pull side.
# AsyncAPI generator — Node CLI
npm i -g @asyncapi/cli
asyncapi generate models \
typescript \
https://dirtfleet.app/asyncapi.yaml \
-o ./src/dirtfleet-events
# Then in your code
import { FlagCreated, WorkOrderCompleted } from "./dirtfleet-events";
function handle(env: { event: string; data: unknown }) {
switch (env.event) {
case "flag.created": return onFlagCreated(env as FlagCreated);
case "workorder.completed":
return onWorkOrderCompleted(env as WorkOrderCompleted);
/* ... */
}
}AsyncAPI generators also exist for Java, Python, Go, .NET — see the official tooling matrix at asyncapi.com/tools.
Event types
What you can subscribe to
flag.created— driver or anomaly raised a flag.flag.resolved— a flag was cleared.hours.logged— new meter reading or state-only entry.asset.created— new asset row.asset.updated— identity, plate, renewals, financials, or lifecycle changed. Payload includes achangedFields[]array for routing.workorder.created/workorder.completed.tool.failure— tool reported BROKEN viaPOST /api/v1/tools/{id}/reportor a BROKEN check-in.tool.checked_out— tool checked out to a user or vehicle. Payload includestoUserId,toVehicleId, optionallinkedAssetId.tool.checked_in— tool checked back in. Payload carries the observedcondition+ resultingnewStatus+ the auto-spawnedworkOrderIdwhen condition is BROKEN.tool.low_stock— a consumable just crossed from above-threshold to at-or-below-threshold via aPOST /tools/{id}/adjust-stockcall. Payload includespreviousStockLevel,newStockLevel, the row-levelminStockLevel(or null for the org-wide fallback), and the effectivethresholdUsed. Only fires once per crossing — going further below or restocking back above does not retrigger.tool.assignment_changed— a tool's owner moved (assignedUserId,assignedVehicleId,assignedYardId, orparentKitIdchanged viaPATCH /tools/{id}). A single PATCH that touches multiple assigned fields produces one delivery whosechangesarray enumerates each kind that moved. Mirrors the internal ASSIGNMENT ToolEvent audit feed — wire this so accounting / HR systems don't have to poll/tools/{id}/events.tool.serviced— a PM service was recorded viaPOST /tools/{id}/mark-serviced. Payload includespreviousLastServicedAt(null on first service),newLastServicedAt,pmIntervalDays, theactorId, and any free-formnote. Mirrors the internalpm_serviced:AUDIT ToolEvent — wire this to asset depreciation systems, OEM warranty trackers, or any CMMS that maintains a parallel service-history ledger.tool.created— a new Tool was created viaPOST /toolsorPOST /tools/batch. Bulk imports emit one delivery per row. Payload includestoolId,name,category,isConsumable,scanToken, optionalpurchaseCost, and theactorId(null on bulk-import paths that don't attribute creation to a specific user). Mirror ofasset.created— keep accounting / asset-register ledgers in lockstep without nightly reconciliation.
Each subscription opts into a CSV of events. Unknown event types in your subscription list are silently dropped at create time (forgiving on typos).
Retries + delivery history
What happens when your receiver is down
Failed deliveries — anything that wasn't a 2xx response — requeue with exponential backoff: 30s → 2m → 8m → 30m → 2h → 8h. After 6 attempts we mark the delivery ABANDONED and stop. The full history (response status, response body truncated to 500 chars, next-attempt time) is queryable via GET /api/v1/webhooks/{id}/deliveries with optional ?status=FAILED,ABANDONED to narrow.
Testing
Synthetic deliveries
POST /api/v1/webhooks/{id}/test fires a synthetic flag.created envelope at your URL with the live signature + an X-Fleetgo-Test: 1 header. We return the receiver's status and the first 500 bytes of its response body — useful for confirming your signature verification works without waiting for a real event.
Idempotency
The receiver pattern that actually works
A failed delivery will retry. If your receiver returns a 200 but crashes before persisting, we think it landed and stop retrying — but the side effect didn't happen. If your receiver returns a 500 after the side effect already happened, we retry and you double-fire. The fix for both is the same: persist the envelope's id to a unique-indexed processed_webhooks table inside the same transaction as your side effect. On retry, the insert collides on the unique index, your handler short-circuits to 200, no double-fire.
More on this in the idempotency-keys post (the same pattern works for both directions).
Working examples
Drop-in Node.js receiver
A complete Express receiver with signature verification + replay-window check + the idempotency pattern is on /docs/api/example — copy-paste into your codebase as the starting point.
← Back to API reference · REST OpenAPI spec → · SDK codegen →