Skip to main content

Overview

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.

Verifying Webhook Signatures

Always verify webhook signatures to ensure requests come from BookingShake and haven’t been tampered with.

How Signature Verification Works

BookingShake signs each webhook request using HMAC SHA256 with your webhook secret. The signature is included in the Bookingshake-Signature header. To verify:
  1. Extract the signature from the request header
  2. Compute the HMAC SHA256 hash of the raw request body using your secret
  3. Compare your computed signature with the received signature
  4. 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.

Code Examples

const express = require('express');
const crypto = require('crypto');

const app = express();

// IMPORTANT: Use raw body for signature verification
app.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, jsonify
import hmac
import hashlib
import json
import os

app = 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', 200

def 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 signature
if ($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 asynchronously
processWebhook($webhook);

// Respond quickly
http_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.

Preventing Duplicate Processing

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.

Implementation Example

const Redis = require('redis');
const redis = Redis.createClient();

app.post('/webhooks/bookingshake', async (req, res) => {
  // ... verify signature first ...

  const idempotencyKey = req.headers['idempotency-key'];

  // Check if we've already processed this webhook
  const alreadyProcessed = await redis.get(`webhook:${idempotencyKey}`);
  if (alreadyProcessed) {
    console.log('Webhook already processed');
    return res.status(200).send('Already processed');
  }

  // Parse and process webhook
  const webhook = JSON.parse(req.body);
  await processWebhook(webhook);

  // Mark as processed (expire after 24 hours)
  await redis.setex(`webhook:${idempotencyKey}`, 86400, 'processed');

  res.status(200).send('OK');
});
app.post('/webhooks/bookingshake', async (req, res) => {
  // ... verify signature first ...

  const idempotencyKey = req.headers['idempotency-key'];

  // Check database for this idempotency key
  const existing = await db.query(
    'SELECT id FROM processed_webhooks WHERE idempotency_key = ?',
    [idempotencyKey]
  );

  if (existing.length > 0) {
    return res.status(200).send('Already processed');
  }

  // Process webhook
  const webhook = JSON.parse(req.body);
  await processWebhook(webhook);

  // Store idempotency key
  await db.query(
    'INSERT INTO processed_webhooks (idempotency_key, processed_at) VALUES (?, NOW())',
    [idempotencyKey]
  );

  res.status(200).send('OK');
});
# Simple in-memory approach (use Redis/DB for production)
processed_webhooks = set()

@app.route('/webhooks/bookingshake', methods=['POST'])
def webhook():
    # ... verify signature first ...

    idempotency_key = request.headers.get('Idempotency-Key')

    # Check if already processed
    if idempotency_key in processed_webhooks:
        return 'Already processed', 200

    # Process webhook
    webhook = request.get_json()
    process_webhook(webhook)

    # Mark as processed
    processed_webhooks.add(idempotency_key)

    return 'OK', 200
Store idempotency keys in Redis, a database, or persistent storage. In-memory storage (like sets) will lose data when your application restarts.

Error Handling and Retries

Delivery Timeout

Your endpoint must respond within 5 seconds with an HTTP status code in the 200-299 range. Any other response is considered a failure.

Retry Policy

BookingShake automatically retries failed webhook deliveries with the following policy:
  • Maximum attempts: 10
  • Backoff strategy: Exponential backoff starting at 10 seconds, up to 1 hour between retries
  • Total retry window: Approximately 3 hours for all retry attempts

Success Response

To acknowledge successful receipt, return an HTTP 200-299 status code:
res.status(200).send('OK');

Handling Failures

After 10 failed delivery attempts, the webhook status changes to error in your dashboard. You’ll see:
  • The error message describing what went wrong
  • The number of failed deliveries (X/10)
  • The last attempted delivery time
What to do when a webhook fails:
  1. Check your webhook endpoint is accessible and responding
  2. Verify your server returns 200-299 status codes within 5 seconds
  3. Check application logs for errors
  4. Test signature verification is working correctly
  5. Fix the issue and monitor the dashboard
Once you fix the issue, the webhook will automatically resume on the next event. You don’t need to recreate it.

Security Requirements

HTTPS Only

All webhook URLs must use HTTPS. HTTP URLs are rejected for security reasons. Examples:
  • https://api.example.com/webhooks/bookingshake
  • https://myapp.herokuapp.com/webhooks
  • http://api.example.com/webhooks (rejected)
  • http://localhost:3000/webhooks (rejected)

Blocked Destinations

For security, webhooks cannot be delivered to:
  • Localhost addresses: 127.0.0.1, ::1, 0.0.0.0, localhost
  • Private IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • Link-local addresses: 169.254.0.0/16
  • Internal domains: .local, .internal, .localhost
  • Metadata servers: Cloud provider metadata endpoints
These restrictions prevent Server-Side Request Forgery (SSRF) attacks.

Best Practices

Respond Quickly

Process webhooks asynchronously and return 200 within 5 seconds. Use queues or background jobs for heavy processing.

Verify Signatures

Always verify the HMAC signature before processing. Never trust the request without verification.

Handle Duplicates

Use the Idempotency-Key header to prevent processing the same webhook twice.

Log Everything

Log all webhook receipts, verifications, and processing results for debugging and auditing.

Monitor Failures

Regularly check your dashboard for failed webhooks. Set up alerts for webhook errors.

Secure Your Secret

Store webhook secrets in environment variables or secure vaults. Never commit them to version control.

Use HTTPS

Always use HTTPS endpoints. HTTP is rejected for security.

Implement Retries

Handle transient failures gracefully. BookingShake retries automatically, but log failures for investigation.

Testing Your Webhook

Using webhook.site

Test your webhook integration without writing code:
1

Get a Test URL

Go to webhook.site to get a unique temporary URL.
2

Create Webhook in BookingShake

In your dashboard, create a new webhook using the webhook.site URL.
3

Trigger an Event

Create or update a contact or account in BookingShake.
4

View the Payload

Return to webhook.site to see the webhook payload, headers, and signature.

Testing Signature Verification

Test your signature verification logic locally:
const crypto = require('crypto');

// Your webhook secret
const secret = 'your-webhook-secret-from-dashboard';

// Example webhook body
const body = JSON.stringify({
  event: 'contact.created',
  timestamp: 1731493800000,
  venue_id: 'venue123',
  data: {
    id: 'contact-456',
    first_name: 'John',
    last_name: 'Doe'
  }
});

// Compute signature
const signature = crypto
  .createHmac('sha256', secret)
  .update(body)
  .digest('hex');

console.log('Expected signature:', signature);

Local Development with ngrok

Test webhooks locally using ngrok:
# Start your local server
node server.js

# In another terminal, expose it with ngrok
ngrok http 3000
Use the HTTPS URL provided by ngrok (e.g., https://abc123.ngrok.io/webhooks/bookingshake) as your webhook URL in BookingShake.

Managing Webhook Secrets

Viewing Your Secret

Your webhook secret is displayed once when you create the webhook. Copy and store it securely in your environment variables.

Regenerating Secrets

If your secret is compromised, regenerate it from the dashboard:
1

Navigate to Webhooks

Go to Settings > Integrations > Webhooks in your dashboard.
2

Edit Webhook

Click on the webhook you want to update.
3

Regenerate Secret

Click “Regenerate Secret” and confirm the action.
4

Update Your Application

Copy the new secret and update your application’s environment variables immediately.
Important: Regenerating a secret immediately invalidates the old one. Update your application before the next webhook delivery to avoid failures.

Zero-Downtime Secret Rotation

For production systems, implement zero-downtime secret rotation:
  1. Create a second webhook with a new secret
  2. Update your application to accept both secrets temporarily
  3. Delete the old webhook after confirming the new one works
  4. Remove the old secret from your application

Webhook Endpoint Checklist

Before going to production, ensure your endpoint:
  • Uses HTTPS (not HTTP)
  • Verifies webhook signatures using HMAC SHA256
  • Handles the Idempotency-Key to prevent duplicate processing
  • Responds within 5 seconds with 200-299 status code
  • Processes webhooks asynchronously (using queues or background jobs)
  • Logs all webhook receipts and processing results
  • Handles all subscribed event types
  • Has error monitoring and alerting set up
  • Stores webhook secrets securely in environment variables
  • Has tests for signature verification logic

Troubleshooting

Signature Verification Fails

Common causes:
  • Using parsed JSON instead of raw body for verification
  • Incorrect secret (check environment variables)
  • Character encoding issues
  • Middleware parsing body before verification
Solution: Always compute the signature on the raw request body before any parsing.

Webhook Status Shows “Error”

Common causes:
  • Endpoint not responding within 5 seconds
  • Returning non-2xx status codes
  • SSL/TLS certificate issues
  • Endpoint not accessible from the internet
Solution: Check your server logs, ensure quick responses, and verify your endpoint is publicly accessible.

Receiving Duplicate Webhooks

Cause: Network issues or retries after timeouts. Solution: Implement idempotency using the Idempotency-Key header.

Not Receiving Webhooks

Checklist:
  1. Verify the webhook is active in the dashboard
  2. Check you’ve selected the correct event types
  3. Trigger an actual event (create/update a contact)
  4. Ensure your endpoint is publicly accessible
  5. Check firewall rules aren’t blocking BookingShake

Need Help?

View Webhook Events

See available webhook events and payload structures

Error Reference

Learn about error codes and status codes

API Documentation

Explore the BookingShake API

Contact Support

Get help from our support team