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');
});
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');
});
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?