Webhooks
Register an endpoint and we'll POST a signed event to it whenever trip, booking or invoice activity happens — so your systems stay in sync without polling.
Events
Subscribe to any of these event types when you create an endpoint:
| Event | When it fires | Payload data |
|---|---|---|
trip.created | A trip is created (via the API, your booking pages, or an accepted quote). | Trip |
trip.assigned | A driver (and/or car) is assigned or reassigned to a trip. | Trip |
trip.status_changed | A trip moves through its lifecycle (confirmed, in progress, completed). | Trip |
trip.cancelled | A trip is cancelled. | Trip |
booking.created | A product/service booking is created. | Booking |
quote.accepted | A customer accepts a quote. | Quote |
invoice.paid | An invoice becomes fully paid. | Payment summary |
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.X-Destination-Signature—t=<timestamp>,v1=<signature>.
Respond with any 2xx status to acknowledge receipt.
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
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, then mark the
delivery failed. Inspect recent attempts with
GET /api/v1/webhooks/{id}/deliveries.
id so retries are harmless.