API Docs
Dashboard

Webhooks

Register an endpoint and we'll POST a signed event to it whenever trip, quote or invoice activity happens — so your systems stay in sync without polling.

Beta. Webhooks are rolling out. The event names and payload shapes below are stable in intent but may gain fields before general availability.

Events

Subscribe to any of these event types when you create an endpoint:

EventWhen it firesPayload data
trip.createdA trip is created (via the API, your booking pages, or an accepted quote).Trip
trip.updatedA trip changes — status, timing, vehicle or passenger details.Trip
trip.completedA trip is marked completed.Trip
trip.cancelledA trip is cancelled.Trip
driver.assignedOne or more drivers are assigned (or reassigned) to a trip.Trip id + driver ids
quote.acceptedA customer accepts a quote.Quote
invoice.createdAn invoice is raised.Invoice
invoice.updatedTrips are attached to or detached from an invoice.Invoice id, change, trip_ids
invoice.paidAn invoice becomes fully paid.Invoice id + status
payment.succeededA payment is recorded against an invoice.Invoice id + amount
payment.failedReserved. Accepted on subscribe, but nothing emits it yet.

Register an endpoint

Create a webhook with an https URL and the events you care about. The response includes a secret — this is the only time it's returned, so store it securely. You'll use it to verify every delivery.

Request

curl -X POST https://app.destination.dev/api/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks",
    "events": ["trip.created", "trip.cancelled"]
  }'

Response

{
  "data": {
    "endpoint_id": 1,
    "url": "https://example.com/webhooks",
    "events": ["trip.created", "trip.cancelled"],
    "status": "enabled",
    "secret": "whsec_182022b9888a6d2f2307..."
  },
  "meta": { "request_id": "req_1a2b3c4d5e6f7a8b" }
}

The event payload

Each delivery is a POST with a JSON body and these headers:

  • X-Destination-Event — the event type.
  • X-Destination-Delivery — the unique event id (same value as id in the body).
  • X-Destination-Signaturet=<timestamp>,v1=<signature>.

Respond with any 2xx status to acknowledge receipt.

Verify against the header's t, not the body's created. Each delivery attempt is signed afresh, so a retry carries a newer t than the created timestamp of the original event. The id stays the same across every attempt — that is the value to de-duplicate on.

POST body

{
  "id": "evt_d49eba5792d7699d5a5d",
  "type": "trip.created",
  "created": 1785148251,
  "data": {
    "trip_id": 4821,
    "reference": "TRIP-4821",
    "status": "confirmed",
    "pickup_at": "2026-08-02 09:15:00",
    "customer_name": "Ada Lovelace"
  },
  "meta": { "business_id": 7 }
}

Verifying signatures

Before trusting a delivery, recompute the signature and compare it. Take the t and v1 values from the X-Destination-Signature header, then compute HMAC-SHA256(t + "." + rawBody, secret) and check it equals v1. Always compare using a constant-time function.

$payload = file_get_contents('php://input');
$header  = $_SERVER['HTTP_X_DESTINATION_SIGNATURE'];
parse_str(strtr($header, ',', '&'), $p); // t=..., v1=...

$expected = hash_hmac(
    'sha256', $p['t'] . '.' . $payload, $secret
);

if (!hash_equals($expected, $p['v1'])) {
    http_response_code(400);
    exit;
}
$event = json_decode($payload, true);
const crypto = require("crypto");

// rawBody must be the exact bytes received.
const header = req.headers["x-destination-signature"];
const p = Object.fromEntries(
  header.split(",").map((kv) => kv.split("="))
);

const expected = crypto
  .createHmac("sha256", secret)
  .update(p.t + "." + rawBody)
  .digest("hex");

if (
  !crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(p.v1)
  )
) {
  return res.status(400).end();
}
const event = JSON.parse(rawBody);

Retries & delivery

The first attempt is made immediately, as the event happens. If your endpoint doesn't return a 2xx, we retry with exponential backoff — roughly 1, 2, 4, 8 and 16 minutes — for up to six attempts in total, then mark the delivery failed. Inspect recent attempts with GET /api/v1/webhooks/{id}/deliveries, which reports status, attempt, next_attempt_at and the last_error for each.

Those intervals are a floor, not a guarantee: retries are drained by a background worker, so a delivery becomes due at the stated time and is sent on the next tick. Deliveries whose endpoint has since been deleted or disabled are marked failed rather than retried.

Make your handler idempotent. A delivery may arrive more than once. De-duplicate on the event id so retries are harmless.