Webhooks
Safra POSTs a signed JSON payload to your HTTPS URL when bookings, trips, schedules, or payments change. Verify the signature before you trust the body.
Create an endpoint
POST /webhooks with a URL and at least one event. The response includes secret once (whsec_…). Store it like an API key.
Create
curl -X POST https://safraway.com/api/partner/v1/webhooks \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"url":"https://crm.example.com/safra/webhooks","events":["booking.created","booking.updated"]}'
| Header | Purpose |
|---|
| Content-Type | application/json |
| User-Agent | Safra-Partner-Webhooks/1.0 |
| X-Safra-Event | Event name, e.g. booking.approved |
| X-Safra-Delivery | Event id (evt_…) |
| X-Safra-Signature | sha256= hex HMAC of the raw body |
Verify the signature
Compute HMAC-SHA256 of the exact raw request body with your signing secret. Prefix with sha256=. Compare using a constant-time function.
Node
const crypto = require("crypto");
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
PHP
$expected = 'sha256='.hash_hmac('sha256', $rawBody, $secret);
hash_equals($expected, $header);
OpenSSL
SECRET=whsec_…
BODY='{"event":"webhook.test"}'
sig=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
# X-Safra-Signature: sha256=$sig
Envelope
JSON
{
"id": "evt_01j…",
"event": "booking.created",
"apiVersion": "2026-08-21",
"occurredAt": "2026-08-21T12:00:00Z",
"data": {
"id": "booking-uuid",
"tripId": "trip-uuid",
"status": "requested",
"seats": 1,
"source": "partner_api",
"boardedAt": null,
"noShow": false
}
}
Events
| Event | When |
|---|
| booking.created | New seat request (app or Partner API) |
| booking.updated | Booking fields changed (including admin edits) |
| booking.approved / rejected / cancelled | Status transitions |
| booking.boarded / booking.no_show | Boarding outcomes |
| trip.created / updated / cancelled / completed | Trip lifecycle |
| schedule.created / updated / deleted | Recurring templates |
| payment.updated | Payment on a company booking changed |
| webhook.test | Dashboard or POST …/test ping |
Retries
- Deliveries are queued jobs. Timeout is 8 seconds.
- HTTP 2xx is success. Anything else retries.
- Up to 5 attempts with backoff 10s, 30s, 120s, 300s.
- Return 2xx quickly; do heavy CRM work after you ack.