# 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