Skip to main content

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.

  1. Enter your endpoint URL. It must use HTTPS.
  2. Select the events you want to receive.
  3. 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:

EventDescription
booking.createdA new booking is made
booking.confirmedA booking is confirmed by the business owner
booking.cancelledA booking is cancelled
booking.rescheduledA booking's date or time is changed — by the business from the dashboard, or by the customer from their manage link
bookings.bulk_cancelledA 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.receivedA payment is successfully completed
payment.refundedA refund is processed
customer.createdA 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

booking.created
{
  "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
      }
    ]
  }
}
json

Payment event example

payment.received
{
  "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"
  }
}
json

Headers

Every webhook request includes the following headers:

HeaderDescription
X-Reservly-SignatureHMAC-SHA256 signature of the payload
X-Reservly-EventThe event type (e.g., booking.created)
X-Reservly-TimestampISO 8601 timestamp of the delivery attempt
Content-Typeapplication/json
User-AgentReservly-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.

Node.js verification example
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');
  }
);
javascript

Retry Behavior

If your endpoint does not respond with a 2xx status code, Reservly retries delivery up to 3 times with exponential backoff:

  • 1st retry1 second after the initial attempt
  • 2nd retry5 seconds after the 1st retry
  • 3rd retry25 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-Signature header 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 timestamp and data fields to implement idempotency in your handler.
  • Monitor delivery history. Check Settings → Integrations for failed deliveries and address any endpoint issues promptly.