Webhook Verification
How to verify webhooks are authentic and haven't been tampered with.
Why verify
Anyone who knows your webhook URL could try to send fake events. To prevent this, each webhook includes a cryptographic signature you must verify.
Never process a webhook without verifying its signature, especially if it performs important actions like creating orders or modifying data.
How it works
- When creating a webhook, a secret unique
- When we send a webhook, we calculate an HMAC-SHA256 hash of the payload using your secret
- We include this hash in the header
X-SalonBookIt-Signature - Your server calculates the same hash and compares it
Signature format
X-SalonBookIt-Signature: sha256=abc123def456...
The signature consists of:
sha256=- Prefix indicating the algorithm- Followed by the HMAC-SHA256 hash in hexadecimal
Get your secret
- Go to Configuration → Integrations
- In the Webhooks section, click on the webhook you want to verify
- Copy the field value Secret
Use environment variables or a secrets manager. Never include it in source code.
Verify the signature
Node.js / JavaScript
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// Extract the hash from the signature
const sigHash = signature.replace('sha256=', '');
// Calculate the expected hash
const expectedHash = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Compare securely (prevents timing attacks)
return crypto.timingSafeEqual(
Buffer.from(sigHash, 'hex'),
Buffer.from(expectedHash, 'hex')
);
}
// In your endpoint
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-salonbookit-signature'];
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
console.error('Invalid signature');
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
console.log('Valid event:', event.type);
// Process the event...
res.json({ received: true });
});
Python
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
"""Verifies the webhook's HMAC signature."""
if not signature.startswith('sha256='):
return False
sig_hash = signature[7:] # Remove 'sha256='
expected_hash = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
# Secure comparison against timing attacks
return hmac.compare_digest(sig_hash, expected_hash)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-SalonBookIt-Signature', '')
payload = request.get_data()
if not verify_signature(payload, signature, WEBHOOK_SECRET):
abort(401, 'Invalid signature')
event = request.get_json()
print(f"Valid event: {event['type']}")
# Process the event...
return {'received': True}
PHP
<?php
$webhookSecret = getenv('WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SALONBOOKIT_SIGNATURE'] ?? '';
function verifySignature(string $payload, string $signature, string $secret): bool
{
if (strpos($signature, 'sha256=') !== 0) {
return false;
}
$sigHash = substr($signature, 7);
$expectedHash = hash_hmac('sha256', $payload, $secret);
return hash_equals($expectedHash, $sigHash);
}
if (!verifySignature($payload, $signature, $webhookSecret)) {
http_response_code(401);
die('Invalid signature');
}
$event = json_decode($payload, true);
error_log('Valid event: ' . $event['type']);
// Process the event...
http_response_code(200);
echo json_encode(['received' => true]);
Ruby
require 'openssl'
require 'sinatra'
WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']
def verify_signature(payload, signature, secret)
return false unless signature.start_with?('sha256=')
sig_hash = signature[7..]
expected_hash = OpenSSL::HMAC.hexdigest('sha256', secret, payload)
Rack::Utils.secure_compare(expected_hash, sig_hash)
end
post '/webhook' do
payload = request.body.read
signature = request.env['HTTP_X_SALONBOOKIT_SIGNATURE'] || ''
unless verify_signature(payload, signature, WEBHOOK_SECRET)
halt 401, 'Invalid signature'
end
event = JSON.parse(payload)
puts "Valid event: #{event['type']}"
# Process the event...
content_type :json
{ received: true }.to_json
end
Verify timestamp (optional)
For extra security, you can verify the webhook isn't too old using the header X-SalonBookIt-Timestamp:
const timestamp = parseInt(req.headers['x-salonbookit-timestamp']);
const now = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 minutes
if (Math.abs(now - timestamp) > tolerance) {
console.error('Webhook too old');
return res.status(401).send('Timestamp expired');
}
This prevents replay attacks where someone tries to resend a previously captured webhook.
Common errors
Signature doesn't match
- Verify you're using the correct secret
- Make sure to use the payload raw (without parsing)
- Don't modify the payload before verifying
Payload is modified
If your framework automatically parses JSON, get the raw body first. In Express:
app.use(express.raw({ type: 'application/json' }))
Encoding issues
Make sure the payload is treated as UTF-8 when calculating the hash.