# 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
```mermaid
sequenceDiagram
participant D as Your App
participant BKB as Bookable
participant WE as Your Webhook Endpoint
Note over D,BKB: Phase 1: Registration
D->>BKB: POST /webhooks {callbackUrl}
BKB->>D: { secretKey }
Note over D: Store secretKey securely
Note over BKB,WE: Phase 2: Event Delivery
loop For each booking event
BKB->>BKB: Generate HMAC-SHA256 signature
BKB->>WE: POST {callbackUrl}
X-API-Key: signature
WE->>WE: Verify signature
alt Valid
WE->>BKB: 204 No Content
else Invalid
WE->>BKB: 401 Unauthorized
end
end
Note over BKB,WE: Failed deliveries retry with exponential backoff
```
## Step 1: Register your webhook
### Obtain an access token
See [Authentication](/getting-started/auth) for the full token flow. Quick reference:
```bash
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
```bash
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"
}'
```
JavaScript
```javascript
const response = await fetch('https://api.bookabletech.com/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
callbackUrl: 'https://your-domain.com/webhooks/booking-notification',
}),
});
const { secretKey } = await response.json();
// Store secretKey securely — you'll need it to verify incoming requests
```
Python
```python
import requests
response = requests.post(
'https://api.bookabletech.com/webhooks',
json={'callbackUrl': 'https://your-domain.com/webhooks/booking-notification'},
headers={'Authorization': f'Bearer {access_token}'}
)
response.raise_for_status()
secret_key = response.json()['secretKey']
# Store secret_key securely — you'll need it to verify incoming requests
```
Java
```java
import java.net.http.*;
import java.net.URI;
String body = """
{ "callbackUrl": "https://your-domain.com/webhooks/booking-notification" }
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.bookabletech.com/webhooks"))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse response.body() to extract secretKey and store it securely
```
C#
```csharp
var payload = new { callbackUrl = "https://your-domain.com/webhooks/booking-notification" };
var response = await client.PostAsJsonAsync("https://api.bookabletech.com/webhooks", payload);
response.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var secretKey = doc.RootElement.GetProperty("secretKey").GetString();
// Store secretKey securely — you'll need it to verify incoming requests
```
**Response:**
```json
{
"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
```bash
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"
}'
```
JavaScript
```javascript
await fetch('https://api.bookabletech.com/webhooks', {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
callbackUrl: 'https://your-new-domain.com/webhooks/booking-notification',
}),
});
```
Python
```python
requests.put(
'https://api.bookabletech.com/webhooks',
json={'callbackUrl': 'https://your-new-domain.com/webhooks/booking-notification'},
headers={'Authorization': f'Bearer {access_token}'}
).raise_for_status()
```
Java
```java
String updateBody = """
{ "callbackUrl": "https://your-new-domain.com/webhooks/booking-notification" }
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.bookabletech.com/webhooks"))
.header("Authorization", "Bearer " + accessToken)
.header("Content-Type", "application/json")
.PUT(HttpRequest.BodyPublishers.ofString(updateBody))
.build();
client.send(request, HttpResponse.BodyHandlers.ofString());
```
C#
```csharp
var payload = new { callbackUrl = "https://your-new-domain.com/webhooks/booking-notification" };
var request = new HttpRequestMessage(HttpMethod.Put, "https://api.bookabletech.com/webhooks");
request.Content = JsonContent.Create(payload);
(await client.SendAsync(request)).EnsureSuccessStatusCode();
```
### Delete your webhook
cURL
```bash
curl -X DELETE https://api.bookabletech.com/webhooks \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
JavaScript
```javascript
await fetch('https://api.bookabletech.com/webhooks', {
method: 'DELETE',
headers: { Authorization: `Bearer ${accessToken}` },
});
```
Python
```python
requests.delete(
'https://api.bookabletech.com/webhooks',
headers={'Authorization': f'Bearer {access_token}'}
).raise_for_status()
```
Java
```java
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.bookabletech.com/webhooks"))
.header("Authorization", "Bearer " + accessToken)
.DELETE()
.build();
client.send(request, HttpResponse.BodyHandlers.ofString());
```
C#
```csharp
(await client.DeleteAsync("https://api.bookabletech.com/webhooks")).EnsureSuccessStatusCode();
```
## 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
| Event | Description |
| --- | --- |
| `booking.created` | A new booking was created |
| `booking.updated` | An existing booking was modified |
| `booking.cancelled` | A booking was cancelled |
### Full endpoint implementation
JavaScript
```javascript
// 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')
);
}
```
Python
```python
# Python / Flask
from flask import Flask, request, jsonify
import hashlib
import hmac
import json
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['BOOKABLE_WEBHOOK_SECRET']
@app.route('/webhooks/booking-notification', methods=['POST'])
def handle_booking_notification():
try:
if not verify_signature(request, WEBHOOK_SECRET):
return jsonify({'success': False, 'error': 'unauthorized'}), 401
data = request.get_json()
event_type = data.get('eventType')
timestamp = data.get('timestamp')
booking = data.get('booking')
if not event_type or not timestamp:
return jsonify({'success': False, 'error': 'validation_error'}), 400
if event_type in ('booking.created', 'booking.updated', 'booking.cancelled'):
print(f"Event: {event_type}, booking ID: {booking[0]['id'] if booking else 'unknown'}")
# Handle event...
else:
print(f"Unknown event type: {event_type}")
return '', 204
except Exception as e:
print(f"Webhook error: {e}")
return jsonify({'success': False, 'error': 'internal_error'}), 500
def verify_signature(req, secret):
signature = req.headers.get('X-API-Key')
if not signature:
return False
payload = json.dumps(req.get_json(), separators=(',', ':'))
expected = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
```
Java
```java
// Spring Boot
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.HexFormat;
import java.util.Map;
@RestController
public class WebhookController {
private final String webhookSecret = System.getenv("BOOKABLE_WEBHOOK_SECRET");
@PostMapping("/webhooks/booking-notification")
public ResponseEntity handleWebhook(
@RequestHeader("X-API-Key") String signature,
@RequestBody String rawBody,
@RequestBody Map body) {
if (!verifySignature(signature, rawBody, webhookSecret)) {
return ResponseEntity.status(401).build();
}
String eventType = (String) body.get("eventType");
if (eventType == null) {
return ResponseEntity.badRequest().build();
}
switch (eventType) {
case "booking.created":
case "booking.updated":
case "booking.cancelled":
System.out.println("Event: " + eventType);
// Handle event...
break;
default:
System.out.println("Unknown event type: " + eventType);
}
return ResponseEntity.noContent().build();
}
private boolean verifySignature(String signature, String payload, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String expected = HexFormat.of().formatHex(mac.doFinal(payload.getBytes()));
return expected.equals(signature);
} catch (Exception e) {
return false;
}
}
}
```
C#
```csharp
// ASP.NET Core — minimal API
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var webhookSecret = Environment.GetEnvironmentVariable("BOOKABLE_WEBHOOK_SECRET")!;
app.MapPost("/webhooks/booking-notification", async (HttpContext ctx) =>
{
ctx.Request.EnableBuffering();
using var reader = new StreamReader(ctx.Request.Body, leaveOpen: true);
var rawBody = await reader.ReadToEndAsync();
ctx.Request.Body.Position = 0;
var signature = ctx.Request.Headers["X-API-Key"].ToString();
if (!VerifySignature(signature, rawBody, webhookSecret))
return Results.Unauthorized();
using var doc = JsonDocument.Parse(rawBody);
var eventType = doc.RootElement.GetProperty("eventType").GetString();
switch (eventType)
{
case "booking.created":
case "booking.updated":
case "booking.cancelled":
Console.WriteLine($"Event: {eventType}");
// Handle event...
break;
default:
Console.WriteLine($"Unknown event type: {eventType}");
break;
}
return Results.NoContent();
});
static bool VerifySignature(string signature, string payload, string secret)
{
var keyBytes = Encoding.UTF8.GetBytes(secret);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
var expected = Convert.ToHexString(
HMACSHA256.HashData(keyBytes, payloadBytes)
).ToLower();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signature)
);
}
app.Run();
```
## Webhook payload
```json
{
"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"
}
]
}
```
### Booking status values
| Status | Description |
| --- | --- |
| `pending` | Awaiting operator confirmation |
| `confirmed` | Booking confirmed |
| `cancelled` | Booking cancelled |
| `completed` | Booking completed |
| `no_show` | Guest did not show up |
## 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:
| 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.
## 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
| 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