Security is critical when receiving webhooks. Always verify that webhook requests actually come from BookingShake before processing them. This guide covers signature verification, idempotency, error handling, and security best practices.
BookingShake signs each webhook request using HMAC SHA256 with your webhook secret. The signature is included in the Bookingshake-Signature header. To verify:
Extract the signature from the request header
Compute the HMAC SHA256 hash of the raw request body using your secret
Compare your computed signature with the received signature
Only process the webhook if they match
1
Extract the Signature
Get the Bookingshake-Signature header from the incoming request.
2
Get Your Secret
Retrieve your webhook secret from the dashboard or your secure environment variables.
3
Compute HMAC SHA256
Calculate the HMAC SHA256 hash of the raw request body using your secret as the key.
4
Compare Signatures
Compare your computed signature with the received signature. If they match, the webhook is authentic.
const express = require('express');const crypto = require('crypto');const app = express();// IMPORTANT: Use raw body for signature verificationapp.use('/webhooks/bookingshake', express.raw({ type: 'application/json' }));app.post('/webhooks/bookingshake', (req, res) => { const signature = req.headers['bookingshake-signature']; const secret = process.env.BOOKINGSHAKE_WEBHOOK_SECRET; // Compute expected signature from raw body const expectedSignature = crypto .createHmac('sha256', secret) .update(req.body) .digest('hex'); // Verify signature if (signature !== expectedSignature) { console.error('Invalid webhook signature'); return res.status(401).send('Invalid signature'); } // Parse webhook payload const webhook = JSON.parse(req.body); console.log('Verified event:', webhook.event); console.log('Data:', webhook.data); // Process webhook asynchronously to respond quickly processWebhook(webhook).catch(err => { console.error('Error processing webhook:', err); }); // Respond immediately (< 5 seconds) res.status(200).send('OK');});async function processWebhook(webhook) { // Your business logic here switch (webhook.event) { case 'contact.created': await syncContactToExternalCRM(webhook.data); break; case 'account.created': await syncAccountToExternalCRM(webhook.data); break; // Handle other events... }}app.listen(3000, () => { console.log('Webhook server listening on port 3000');});
from flask import Flask, request, jsonifyimport hmacimport hashlibimport jsonimport osapp = Flask(__name__)@app.route('/webhooks/bookingshake', methods=['POST'])def webhook(): # Get signature from header signature = request.headers.get('Bookingshake-Signature') secret = os.environ.get('BOOKINGSHAKE_WEBHOOK_SECRET') # Get raw request body raw_body = request.data # Compute expected signature expected_signature = hmac.new( secret.encode('utf-8'), raw_body, hashlib.sha256 ).hexdigest() # Verify signature if signature != expected_signature: print('Invalid webhook signature') return 'Invalid signature', 401 # Parse webhook payload webhook = request.get_json() print(f"Verified event: {webhook['event']}") print(f"Data: {webhook['data']}") # Process webhook asynchronously process_webhook(webhook) # Respond quickly return 'OK', 200def process_webhook(webhook): # Your business logic here event_type = webhook['event'] if event_type == 'contact.created': sync_contact_to_crm(webhook['data']) elif event_type == 'account.created': sync_account_to_crm(webhook['data']) # Handle other events...if __name__ == '__main__': app.run(port=3000)
<?php// Get raw POST body (IMPORTANT: Don't use php://input with JSON parsing first)$rawBody = file_get_contents('php://input');// Get signature from header$signature = $_SERVER['HTTP_BOOKINGSHAKE_SIGNATURE'] ?? '';$secret = getenv('BOOKINGSHAKE_WEBHOOK_SECRET');// Compute expected signature$expectedSignature = hash_hmac('sha256', $rawBody, $secret);// Verify signatureif ($signature !== $expectedSignature) { error_log('Invalid webhook signature'); http_response_code(401); exit('Invalid signature');}// Parse webhook payload$webhook = json_decode($rawBody, true);error_log("Verified event: " . $webhook['event']);error_log("Data: " . json_encode($webhook['data']));// Process webhook asynchronouslyprocessWebhook($webhook);// Respond quicklyhttp_response_code(200);echo 'OK';function processWebhook($webhook) { // Your business logic here switch ($webhook['event']) { case 'contact.created': syncContactToCRM($webhook['data']); break; case 'account.created': syncAccountToCRM($webhook['data']); break; // Handle other events... }}
Critical: Always use the raw request body for signature verification, not the parsed JSON. Parsing and re-stringifying JSON may change whitespace or ordering, causing signature mismatches.
Webhooks may occasionally be delivered more than once due to network issues or retries. Use the Idempotency-Key header to ensure you only process each webhook once.