Webhooks

CarFleet POSTs to your URL when something happens in a workspace, so you are told instead of polling for it. Register an endpoint in Settings → API & MCP.

Two facts to design for

Both are cheap to handle now and expensive to discover in production.

Delivery is at-least-once

If your server commits the event and then times out, that looks exactly like a failure from our side and we will send it again. Every payload carries an id that is stable across retries — store it and ignore a repeat. Nobody can promise exactly-once over HTTP; we would rather say so than let you find out.

Order is not guaranteed

A retried event can arrive after a newer one. Decide what is true from the data in the payload, never from the order of arrival.

The request

delivery
POST https://your-server.example/carfleet
content-type: application/json
carfleet-event: booking.confirmed
carfleet-delivery: 6f1c…
carfleet-signature: t=1760000000,v1=9a3f…

{
  "id": "6f1c…",
  "event": "booking.confirmed",
  "createdAt": "2026-08-18T09:14:03.221Z",
  "data": { "bookingId": "…", "customerId": "…", "vehicleId": "…" }
}

carfleet-delivery is the same value as id in the body, so a proxy log is enough to match a delivery without parsing it.

Verifying the signature

Do this before you trust anything in the body. The signing secret is shown once, when you create the endpoint.

The signed string is <timestamp>.<raw body> — the timestamp is inside the signature on purpose. Signing the body alone would let anyone who captured one delivery replay it for ever.

verify.js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => {
      const i = kv.indexOf("=");
      return [kv.slice(0, i), kv.slice(i + 1)];
    }),
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t)) return false;
  // Two-sided: a clock running ahead of ours is ordinary skew, not an attack.
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Verify the raw body

Before any JSON parsing. Re-serialising changes the bytes and the signature will not match — this is the single most common reason a correct implementation appears broken.

What we expect back

Answer 2xx, and answer quickly — we wait ten seconds. Acknowledge first, do the work afterwards.

Anything else is a failure and we retry: 1m, 5m, 15m, 1h, 3h, 6h, 12h, 24h — eight attempts, then the delivery is marked failed and left alone.

A 3xx is a failure, not a success. We do not follow redirects; following one would post a signed payload to a host you never nominated.

An endpoint that fails twenty times in a row is switched off, with the time recorded, and the screen says so. Silence always has a visible reason.

Events

Every domain event CarFleet raises is available. Leave an endpoint’s event list empty to receive all of them.

GroupEvents
Bookingsbooking.confirmedbooking.completedbooking.delivery_duebooking.return_due
Moneypayment.capturedpayment.refundedpayment.deposit_heldpayment.canceledinvoice.overdue
Quotesquote.sentquote.viewedquote.acceptedquote.declinedquote.expired
Saleslead.createddeal.createddeal.stage_changed
Fleetvehicle.maintenance_duevehicle.document_expiringvehicle.idle
Operationsdelivery.assignedtask.assignedtask.due_soontask.overdueinbox.message_receivedinbox.assigned
Investorsinvestor.payout_readyinvestor.payout_generatedinvestor.report_due
Documentsdocument.esign_timeout
Healthintegration.failed

integration.failed — the one worth wiring first

Fires when one of the workspace’s own integrations has stopped working: e-signature, accounting, a payment gateway, WhatsApp.

payload
{
  "event": "integration.failed",
  "data": {
    "integration": "zoho_sign",
    "detail": "invalid_code",
    "hint": "reconnect_required",
    "failingSince": "2026-08-15T10:58:33.173Z"
  }
}

detail is the provider’s own words, verbatim. hint is what to do about it.

We ship this because we have twice needed it ourselves: e-signature was silent for twelve days and WhatsApp for two, and in both cases the system knew and nobody was told. Point it at whatever you already watch.

Re-sent once a day while the integration stays broken, so a long outage is a heartbeat rather than a flood — and so it cannot be silenced by a stale flag, which is exactly how the WhatsApp alert made itself unfireable.

Money in payloads

Amounts are fils — AED × 100. 784140 is AED 7,841.40. There are no decimal amounts anywhere in this API.

Plain text, for an agent: /docs/webhooks.md.