Webhooks
Webhooks let Reservly push real-time notifications to your app when events happen — new bookings, payments, cancellations, etc. Instead of polling our API, your server receives an HTTP POST automatically.
Setting Up Webhooks
Configure webhooks from your dashboard in Settings → Integrations → Webhooks.
- Enter your endpoint URL. It must use HTTPS.
- Select the events you want to receive.
- Click Add Webhook. The signing secret is shown once, immediately after — copy it before you navigate away.
After adding a webhook, use the paper-plane Test webhook button on its row to send a sample payload and verify your endpoint is reachable.
A webhook with no signing secret is not delivered at all. Reservly fails closed rather than sending an unsigned payload, and records the refusal in that webhook's delivery history. If events are not arriving and the history shows no signing secret, generate one from the same tab.
Event Types
Subscribe to any combination of the following events:
| Event | Description |
|---|---|
booking.created | A new booking is made |
booking.confirmed | A booking is confirmed by the business owner |
booking.cancelled | A booking is cancelled |
booking.rescheduled | A booking's date or time is changed — by the business from the dashboard, or by the customer from their manage link |
bookings.bulk_cancelled | A bulk or cascade cancellation ran (multi-select cancel, closing a date range, cancelling or deleting an event, service, rental, class or resource). One event per operation — never one per booking — carrying the affected booking ids (first 100, withtotal_count holding the real number), the reason, and a scope string naming which bulk path fired. No customer details are included — look bookings up by id if you need them. The per-booking booking.cancelledevent still fires for single cancellations only. |
payment.received | A payment is successfully completed |
payment.refunded | A refund is processed |
customer.created | A new customer profile is created |
Scope of booking.rescheduled: it fires for service and rental bookings moved from the dashboard or by the customer. Moving a whole event to a new date or time shifts all of its attendee bookings at once; those attendees are notified by email broadcast and no per-booking webhook is emitted for them. If you need to react to an event moving, reconcile from the event rather than waiting on per-attendee deliveries.
Payload Format
Every webhook delivery is a POST request with a JSON body. The top-level structure includes the event type, a timestamp, your business identity, and the event-specific data.
Booking event example
{
"event": "booking.created",
"timestamp": "2026-04-15T10:30:00.000Z",
"business": {
"id": "uuid",
"slug": "luxe-salon"
},
"data": {
"booking_id": "uuid",
"date": "2026-04-15",
"start_time": "10:00",
"end_time": "11:00",
"status": "pending",
"customer_name": "Jane Smith",
"customer_email": "jane@example.com",
"services": [
{
"name": "Haircut",
"duration_minutes": 60,
"price": 45
}
]
}
}jsonPayment event example
{
"event": "payment.received",
"timestamp": "2026-04-15T10:31:00.000Z",
"business": {
"id": "uuid",
"slug": "luxe-salon"
},
"data": {
"booking_id": "uuid",
"amount_cents": 4500,
"currency": "USD",
"provider": "stripe",
"status": "paid",
"customer_name": "Jane Smith",
"customer_email": "jane@example.com"
}
}jsonHeaders
Every webhook request includes the following headers:
| Header | Description |
|---|---|
X-Reservly-Signature | HMAC-SHA256 signature of the payload |
X-Reservly-Event | The event type (e.g., booking.created) |
X-Reservly-Timestamp | ISO 8601 timestamp of the delivery attempt |
Content-Type | application/json |
User-Agent | Reservly-Webhooks/1.0 |
Signature Verification
To verify that a webhook was genuinely sent by Reservly, compute an HMAC-SHA256 hash of the raw request body using that webhook's signing secret, hex-encode the digest, and compare it to the X-Reservly-Signature header.
Each webhook has its own signing secret — 32 random bytes, hex-encoded — generated when you create the webhook in Settings → Integrations. It is displayed once, at that moment: copy it then and store it with your other server-side secrets. If you lose it, open the same tab and use Rotate signing secret on that webhook to mint a replacement; the previous secret stops verifying immediately, so update your endpoint at the same time. Your API keys are unrelated to webhook signatures — they authenticate requests you make to Reservly, not deliveries Reservly makes to you.
Sign the exact bytes you received, before any JSON parsing. Re-serialising a parsed body often reproduces the same bytes and so appears to work — but it is not guaranteed to, and any single byte of difference makes the signature fail. Verifying against the raw body removes that whole class of intermittent failure.
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
// rawBody is a Buffer of the exact bytes received — never a parsed object.
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
const received = Buffer.from(String(signature || ''), 'hex');
// timingSafeEqual throws when the lengths differ, so check length first —
// otherwise a short or malformed header crashes the handler instead of
// being rejected.
if (received.length !== expected.length) return false;
return crypto.timingSafeEqual(received, expected);
}
// In your Express handler — express.raw, NOT express.json, so the
// original bytes survive to the verification step:
app.post(
'/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const isValid = verifyWebhook(
req.body, // Buffer of the raw bytes
req.headers['x-reservly-signature'],
process.env.RESERVLY_WEBHOOK_SECRET
);
if (!isValid) return res.status(401).send('Invalid signature');
const event = JSON.parse(req.body.toString('utf8'));
// Process the event...
res.status(200).send('OK');
}
);javascriptRetry Behavior
If your endpoint does not respond with a 2xx status code, Reservly retries delivery up to 3 times with exponential backoff:
- 1st retry — 1 second after the initial attempt
- 2nd retry — 5 seconds after the 1st retry
- 3rd retry — 25 seconds after the 2nd retry
Each attempt has a 15-second timeout.
Only 5xx responses and network failures trigger retries. Every other outcome is treated as permanent and is not retried — that includes 4xx and also 3xx, because redirects are never followed. In particular, an endpoint behind SSO or an authentication proxy answers 401 or 403, which means the event is attempted once and dropped: point webhooks at a genuinely public endpoint. You can view the full delivery history in Settings → Integrations.
Best Practices
- Respond with 200 quickly. Acknowledge the webhook immediately and process the event asynchronously (e.g., via a queue). Long-running handlers risk hitting the 15-second timeout.
- Verify signatures on every request. Always check the
X-Reservly-Signatureheader to confirm the payload was not tampered with. - Handle duplicate deliveries. Network issues can cause the same event to be delivered more than once. Use the
timestampanddatafields to implement idempotency in your handler. - Monitor delivery history. Check Settings → Integrations for failed deliveries and address any endpoint issues promptly.