REST API webhooks
Webhooks let your application get notified automatically when something happens in your SimplyBook.me company account — for example when a new client registers or a booking is cancelled — instead of having to poll the API for changes.
This page covers the webhook subscription endpoints of our REST API. For authentication headers (X-Company-Login, X-Token), see Authentication.
Managing webhook subscriptions
Subscriptions are managed with three endpoints.
List subscriptions
GET /admin/webhooks
Optional query parameter is_active (0 or 1) filters by status. When omitted, only active subscriptions are returned.
Create a subscription
POST /admin/webhooks
Content-Type: application/json
{
"url": "https://example.com/webhook",
"event": "new_client"
}
url must be a valid absolute URL (http/https) that will receive the callback. event is one of the values listed below.
Up to 5 active subscriptions are allowed per event. Creating another one for an event that already has 5 active subscriptions automatically deactivates the oldest one for that event.
Remove a subscription
DELETE /admin/webhooks/{id}
Removes the subscription with the given id (the id is returned when the subscription is created, and is included in the list response).
Available events
| Event | Fired when |
|---|---|
| new_booking | a new booking is created |
| change_booking | a booking is changed (rescheduled, provider/service changed, etc.) |
| cancel_booking | a booking is cancelled |
| new_client | a new client is created |
| change_client | a client's data is updated |
| delete_client | a client is deleted |
| new_offer | a new offer/quote is created |
| new_invoice | a new invoice is created |
Example
Subscribe to new client notifications:
POST /admin/webhooks
Content-Type: application/json
X-Company-Login: your-company-login
X-Token: your-token
{
"url": "https://example.com/webhook",
"event": "new_client"
}
Response:
{
"id": 12,
"url": "https://example.com/webhook",
"event": "new_client",
"is_active": 1,
"changed": "2026-08-27 12:00:00"
}
From then on, every time a new client is created, we will send a POST request with the event data as a JSON body to https://example.com/webhook.
Verifying the webhook signature
If an API secret key is set up for your company, every webhook request includes an X-Signature header so you can verify it actually came from SimplyBook.me and was not tampered with in transit.
Before sending, we add two fields to the JSON payload:
- webhook_timestamp — unix timestamp of when the request was sent
- signature_algo — the hashing algorithm used (currently sha256)
The X-Signature header is an HMAC of the exact JSON body, computed with your account's secret key.
Your secret key is shown in the admin panel under Custom Features → API → Settings (the same place your API login key is shown), next to field api_secret_key.
Important: compute the HMAC over the raw request body before it gets parsed/re-serialized — parsing and re-encoding JSON can change whitespace/key order and break the signature check. Always use a constant-time comparison function for the check, never ==/===, to avoid timing attacks.
PHP
<?php
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$secret = 'YOUR_SECRET_KEY';
$expected = hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(400);
exit('Invalid signature');
}
$data = json_decode($rawBody, true);
// ... process the event ...
http_response_code(200);
Node.js
const crypto = require('crypto');
function signaturesMatch(expectedHex, actualHex) {
const expected = Buffer.from(expectedHex, 'utf8');
const actual = Buffer.from(actualHex, 'utf8');
if (expected.length !== actual.length) return false;
return crypto.timingSafeEqual(expected, actual);
}
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const secret = 'YOUR_SECRET_KEY';
const signature = req.headers['x-signature'] || '';
const rawBody = req.body; // Buffer, exact bytes of the request
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
if (!signaturesMatch(expected, signature)) {
return res.status(400).send('Invalid signature');
}
const data = JSON.parse(rawBody);
// ... process the event ...
res.sendStatus(200);
});
Python
import hashlib
import hmac
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
secret = 'YOUR_SECRET_KEY'
signature = request.headers.get('X-Signature', '')
raw_body = request.get_data() # exact bytes of the request body
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(400, 'Invalid signature')
data = request.get_json()
# ... process the event ...
return '', 200
Delivery behaviour
- Your endpoint should respond within a few seconds and return a 2xx status code to acknowledge the request.
- Redirects are not followed — the URL must respond directly.
- If delivery fails, we automatically retry with increasing delays. If it keeps failing, we eventually stop retrying that event.
See also