REST API webhooks

From SimplyBook.me



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