The Bookable webhook system delivers real-time notifications to your endpoint when booking events occur — such as new bookings, updates, and cancellations.
- Register your callback URL via the API and receive a
secretKey - Store the secret key securely — you'll use it to verify every incoming request
- Implement an HTTPS endpoint that accepts POST requests and verifies the HMAC-SHA256 signature
- Respond with
204 No Contenton success
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"
}'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.
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"
}'curl -X DELETE https://api.bookabletech.com/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Your endpoint must accept POST requests, verify the HMAC-SHA256 signature, process the event, and respond with 204 No Content.
| Event | Description |
|---|---|
booking.created | A new booking was created |
booking.updated | An existing booking was modified |
booking.cancelled | A booking was cancelled |
// 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;
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, booking } = req.body;
if (!eventType || !timestamp) {
return res.status(400).json({ success: false, error: 'validation_error' });
}
switch (eventType) {
case 'booking.created':
case 'booking.updated':
case 'booking.cancelled':
console.log(`Event: ${eventType}, booking ID: ${booking[0]?.id}`);
// 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')
);
}{
"eventType": "booking.created",
"timestamp": "2025-06-17T10:30:00Z",
"booking": [
{
"id": "12345678-1234-1234-1234-123456789012",
"venueId": "87654321-4321-4321-4321-210987654321",
"date": "2025-06-25",
"time": "19:30:00",
"partySize": 4,
"status": "confirmed",
"reference": "REF-20250617-001",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"phone": "+1234567890",
"duration": 120,
"notes": "Window table preferred",
"createdDate": "2025-06-17T09:15:00Z",
"lastUpdate": "2025-06-17T10:30:00Z"
}
]
}| Status | Description |
|---|---|
pending | Awaiting operator confirmation |
confirmed | Booking confirmed |
cancelled | Booking cancelled |
completed | Booking completed |
no_show | Guest did not show up |
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:
- Read the raw request body as a string (before any JSON parsing)
- Compute
HMAC-SHA256(secretKey, rawBody)and hex-encode it - Compare the result to the
X-API-Keyheader 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.
Your endpoint should return appropriate HTTP status codes:
| Code | Meaning |
|---|---|
204 | Success — event processed |
400 | Bad request — invalid payload |
401 | Unauthorized — signature invalid |
500 | Server 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.
- HTTPS only — never register an HTTP callback URL
- Verify every request — always validate the signature before processing
- Respond quickly — return
204immediately and process the event asynchronously if needed - Handle duplicates — retries can deliver the same event more than once; make your handler idempotent
- Log everything — store raw payloads for debugging and auditing
| Symptom | Check |
|---|---|
| Not receiving events | Is your callback URL publicly reachable over HTTPS? Does it return 204? |
| Signature mismatch | Are you hashing the raw body before JSON parsing? Is the stored secret correct? |
| Auth failures | Are your OAuth2 credentials valid? Is audience set to api.bookabletech.com? |
Support: hello@bookabletech.com