- On the Integrations page, under Webhooks, click Add webhook (the Project needs a website shop with Business Apps, the Business Apps API or the Wi-Fi Hotspot API, and you must be the Owner or an Admin)
- Enter an
https://URL reachable from the internet, and pick the events you want (none means every event) - Copy the Signing secret: it is shown only once
- Click Test to send a
pingevent to your URL
| Event | When |
|---|---|
order.created, order.updated, bill.closed, booking.created, shift.closed and more | Something changed in a shop, a website's or the API's, whether from the Console, the staff app or the API (carries shopId) |
order.payment_claimed, booking.payment_claimed, booking.cancelled | A customer on the website says they paid, or cancels their booking |
voucher.used | A Wi-Fi code is used for the first time (the code itself is never sent; the note you gave it, such as a room number, is) |
guest.signed_in | A guest left an email or phone on the sign-in page (returning says whether they came before) |
box.offline, box.online | A Wi-Fi box stopped calling for over 2 minutes, and when it is back |
Each event is a JSON POST. Your system must answer 2xx within 10 seconds. If it does not, we retry automatically, waiting longer each time, for about a day. Deliveries shows what was sent.
{"id":"evt_…","type":"order.created","createdAt":"2026-10-13T09:30:00Z","shopId":"…","data":{…}}Check that a webhook really comes from us
Each one carries X-TH-Signature: t=<time>,v1=<signature>. The signature is the HMAC-SHA256 of <time>.<body> with your signing secret. Compute it over the raw body you received, before parsing the JSON.
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const mac = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(parts.v1));
}import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
mac = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, parts["v1"])Events may arrive out of order: sort them by createdAt, and use id to skip duplicates, since an event can be delivered again.
If something goes wrong
Adding a webhook says the URL must be on the internet
Use an https:// URL that can be opened from outside, not an internal IP or localhost.
Deliveries shows “Failed”
Check the HTTP status your URL answered. We keep retrying until it succeeds or the retries run out. Fix your receiver, then click Test to check.