Skip to content
Last updated

Webhook Guide

The Bookable webhook system delivers real-time notifications to your endpoint when booking events occur — such as new bookings, updates, and cancellations.

Integration flow

  1. Register your callback URL via the API and receive a secretKey
  2. Store the secret key securely — you'll use it to verify every incoming request
  3. Implement an HTTPS endpoint that accepts POST requests and verifies the HMAC-SHA256 signature
  4. Respond with 204 No Content on success
Your Webhook EndpointBookableYour AppYour Webhook EndpointBookableYour AppPhase 1: RegistrationStore secretKey securelyPhase 2: Event Deliveryalt[Valid][Invalid]loop[For each booking event]Failed deliveries retry with exponential backoffPOST /webhooks {callbackUrl}{ secretKey }Generate HMAC-SHA256 signaturePOST {callbackUrl}X-API-Key: signatureVerify signature204 No Content401 Unauthorized

Environments

Webhooks work in both sandbox and production. You can exercise the entire flow — registration, delivery, and signature verification — in sandbox before going live.

SandboxProduction
Base URLhttps://api-sandbox.bookabletech.comhttps://api.bookabletech.com
Auth endpointhttps://auth-sandbox.bookabletech.com/oauth/tokenhttps://auth.bookabletech.com/oauth/token

The examples below use the production URLs — substitute the sandbox host and your sandbox credentials to test.

Testing in sandbox

In sandbox there is no external reservation system driving events, so your own booking actions generate the webhooks. Once you have registered a callback URL (using your sandbox credentials against https://api-sandbox.bookabletech.com), any booking you create, amend, or cancel through the sandbox API delivers a signed notification to that callback — the same payload and X-API-Key HMAC-SHA256 signature you receive in production, with no external system required.

Each notification is sent with eventType: booking.updated; the booking's current lifecycle state is carried in its status field (for example confirmed or cancelled).

End to end:

  1. Register your callback URL against the sandbox base URL (Step 1 below) and store the returned secretKey.
  2. Create, amend, or cancel a booking via the sandbox API.
  3. Your endpoint receives the POST — verify the X-API-Key signature with your secretKey (Step 3 below).

Step 1: Register your webhook

Obtain an access token

See Authentication for the full token flow. Quick reference:

curl -X POST https://auth.bookabletech.com/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "audience": "api.bookabletech.com"
  }'

Register your callback URL

curl -X POST https://api.bookabletech.com/webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://your-domain.com/webhooks/booking-notification"
  }'

Response:

{
  "secretKey": "123e4567-e89b-12d3-a456-426655440000",
  "callbackUrl": "https://your-domain.com/webhooks/booking-notification"
}

⚠️ Store the secret key securely

The secretKey is returned only once at registration. Store it in a secrets manager or environment variable — never hardcode it.

Update your callback URL

curl -X PUT https://api.bookabletech.com/webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://your-new-domain.com/webhooks/booking-notification"
  }'

Delete your webhook

curl -X DELETE https://api.bookabletech.com/webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Step 2: Implement your webhook endpoint

Your endpoint must accept POST requests, verify the HMAC-SHA256 signature, process the event, and respond with 204 No Content.

Supported event types

Bookable POSTs a JSON BookingNotification. The eventType field is the discriminator and takes one of two values:

eventTypePopulated fieldTrigger
booking.updatedbookings (array)A booking was created, changed, or cancelled — the lifecycle state is carried in each booking's status field (confirmed, cancelled, …)
message.receivedmessages (array)The venue operator sent a message on the booking conversation (operator_to_partner only)

There is no separate booking.created/booking.cancelled event — creation and cancellation are both delivered as booking.updated with the relevant status.

Delivery is at-least-once

The same event may be delivered more than once (failed deliveries retry — see retries). Make your handler idempotent: deduplicate booking events by the booking id and messages by the message id.

Full endpoint implementation

// Node.js / Express
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.BOOKABLE_WEBHOOK_SECRET;

// Delivery is at-least-once, so record every id before applying side effects.
// Durable in production (e.g. a unique insert in your datastore); in-memory Set shown for brevity.
const processedIds = new Set();
function markProcessed(id) {
  if (processedIds.has(id)) return false; // already handled — skip
  processedIds.add(id);                    // replace with an insert-if-absent in your datastore
  return true;
}

app.post('/webhooks/booking-notification', (req, res) => {
  try {
    if (!verifySignature(req.headers['x-api-key'], req.body, WEBHOOK_SECRET)) {
      return res.status(401).json({ success: false, error: 'unauthorized' });
    }

    const { eventType, timestamp, bookings, messages } = req.body;

    if (!eventType || !timestamp) {
      return res.status(400).json({ success: false, error: 'validation_error' });
    }

    switch (eventType) {
      case 'booking.updated':
        // Iterate every affected booking. bookings[].status carries the lifecycle
        // state (confirmed, cancelled, ...). Dedupe on booking.id before side effects.
        for (const booking of bookings ?? []) {
          if (!markProcessed(booking.id)) continue;
          console.log(`booking.updated: ${booking.id} (${booking.status})`);
          // Handle event...
        }
        break;
      case 'message.received':
        // Iterate every message; dedupe on message.id before side effects.
        // Reply by emailing message.replyTo, or POST a message via the Messaging API.
        for (const message of messages ?? []) {
          if (!markProcessed(message.id)) continue;
          console.log(`message.received: ${message.id} for booking ${message.bookingId}`);
          // Handle event...
        }
        break;
      default:
        console.warn(`Unknown event type: ${eventType}`);
    }

    res.status(204).end();

  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ success: false, error: 'internal_error' });
  }
});

function verifySignature(signature, body, secret) {
  if (!signature) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(body))
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

Webhook payload

booking.updated

The affected booking(s) are in the bookings array.

{
  "eventType": "booking.updated",
  "timestamp": "2026-06-17T10:30:00Z",
  "bookings": [
    {
      "id": "29|CO|275cc44dd2e2496fba44857c9257443a|B",
      "compositeId": "29|CO|275cc44dd2e2496fba44857c9257443a|d99128c546b34b619c4477b712869f2b",
      "date": "2026-06-25",
      "time": "19:30:00",
      "partySize": 4,
      "status": "confirmed",
      "reference": "REF-20260617-001",
      "firstName": "John",
      "lastName": "Doe",
      "email": "john.doe@example.com",
      "phone": "+1234567890",
      "duration": 120,
      "createdDate": "2026-06-17T09:15:00Z",
      "lastUpdate": "2026-06-17T10:30:00Z"
    }
  ]
}

message.received

One or more messages for a single booking are in the messages array. Fires only when the venue operator sends a message (operator_to_partner). Deduplicate on id.

{
  "eventType": "message.received",
  "timestamp": "2026-06-17T11:05:00Z",
  "messages": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "bookingId": "29|CO|6a3e866a4989b3b73d78f011|B",
      "body": "We've noted your high-chair request.",
      "subject": "Re: Table for Jane Smith",
      "senderName": "Abbey Inn",
      "senderEmail": "venue@example.com",
      "direction": "operator_to_partner",
      "replyTo": "booking-op7k2@mail.bookabletech.com",
      "attachments": [
        { "filename": "floorplan.pdf", "contentType": "application/pdf" }
      ],
      "sentAt": "2026-06-17T11:05:00Z"
    }
  ]
}

Replying to a message: email the replyTo alias from anywhere (attachments supported) and Bookable relays it to the venue operator, or POST /bookings/{bookingId}/messages via the Messaging API. Attachment bytes are exchanged over email, not in the webhook — attachments here is metadata only.

Booking status values

StatusDescription
pendingAwaiting operator confirmation
in_progressEnquiry received and assigned but not yet confirmed
confirmedBooking confirmed
cancelledBooking cancelled or rejected
deletedBooking deleted from the system
lostBooking was not completed before its scheduled date

Signature verification

Every webhook request includes an X-API-Key header containing an HMAC-SHA256 hex digest of the raw request body, signed with your secretKey.

Verification steps:

  1. Read the raw request body as a string (before any JSON parsing)
  2. Compute HMAC-SHA256(secretKey, rawBody) and hex-encode it
  3. Compare the result to the X-API-Key header using a timing-safe comparison

⚠️ Use the raw body

Always compute the signature on the raw request body string, not a re-serialised version of the parsed JSON — key ordering and whitespace must be identical.


Error handling & retries

Your endpoint should return appropriate HTTP status codes:

CodeMeaning
204Success — event processed
400Bad request — invalid payload
401Unauthorized — signature invalid
500Server error — processing failed

Failed deliveries are retried with exponential backoff: 1, 2, 4, 8, 16, and 32 minutes. After 6 attempts the event is dropped and logged for review.


Best practices

  1. HTTPS only — never register an HTTP callback URL
  2. Verify every request — always validate the signature before processing
  3. Respond quickly — return 204 immediately and process the event asynchronously if needed
  4. Handle duplicates — retries can deliver the same event more than once; make your handler idempotent
  5. Log everything — store raw payloads for debugging and auditing

Troubleshooting

SymptomCheck
Not receiving eventsIs your callback URL publicly reachable over HTTPS? Does it return 204?
Signature mismatchAre you hashing the raw body before JSON parsing? Is the stored secret correct?
Auth failuresAre your OAuth2 credentials valid? Is audience set to api.bookabletech.com?

Support: hello@bookabletech.com