Create a webhook subscription
Register a new webhook URL to receive booking events. Maximum 5 webhooks per restaurant.
The secret is auto-generated and returned in the response — use it to verify webhook signatures.
Webhook HTTP Headers
Each delivery includes these headers:
X-Webhook-Id: Unique event ID for idempotency. The same value appears asidin the JSON payload — deduplicate deliveries on it.X-Webhook-Event: The event type (e.g.,booking.created,booking.updated).X-Webhook-Timestamp: Unix timestamp of the delivery.X-Webhook-Delivery-Id: The delivery row ID. Best-effort ordering — a higher value was dispatched later, but retries may arrive out of order.X-Webhook-Signature(legacy):sha256=<HMAC-SHA256 hex digest>over the raw JSON body, using the webhooksecret.Tableo-Signature(recommended, anti-replay): Stripe-style headert=<unix_timestamp>,v1=<HMAC-SHA256 hex of "{timestamp}.{raw_body}" using the secret>.
Verifying Signatures
Verify the signature over the RAW request body BEFORE parsing JSON.
Both X-Webhook-Signature and Tableo-Signature are computed over the
exact same raw bytes that were sent — do not re-serialize a parsed
object, or the digest will not match.
Prefer the anti-replay Tableo-Signature scheme:
import hmac, hashlib, time
TOLERANCE_SECONDS = 5 * 60 # reject replays older than 5 minutes
def verify_tableo_signature(raw_body: bytes, header: str, secret: str) -> bool:
# header looks like: t=1718000000,v1=abcdef0123...
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp = parts.get("t")
signature = parts.get("v1")
if not timestamp or not signature:
return False
# 1. Reject replays outside the tolerance window.
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
# 2. Recompute the HMAC over "{timestamp}.{raw_body}".
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
# 3. Constant-time compare.
return hmac.compare_digest(expected, signature)The legacy X-Webhook-Signature header is still sent for backwards
compatibility. If you cannot access the raw body and must fall back to
it, ensure your framework exposes the unmodified bytes (not a
re-serialized object):
import hmac, hashlib
def verify_legacy_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)Delivery Semantics
- At-least-once delivery. A single event may be delivered more than once — always deduplicate by
X-Webhook-Id(or the payloadid). - Retries. Any non-2xx response or network error is treated as a failure and retried automatically, up to
max_attempts(3). Retries may arrive out of order relative to other events. - No global sequence. There is no monotonic global event sequence. Use
X-Webhook-Delivery-Idfor best-effort ordering within a single webhook.
Events
See the Events reference for the full list of event types and when each one fires.
Authorization
bearerAuth Supports two token types:
- Restaurant tokens — API tokens with
restaurant-apiability, issued per restaurant. - User tokens — Personal access tokens with
mobile-accessability (e.g. for the mobile app). When managing multiple restaurants, includeX-Restaurant-Id: <id>header.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
curl -X POST "https://devrms.tabdevx.com/api/restaurant/webhooks" \ -H "Content-Type: application/json" \ -d '{ "url": "https://api.example.com/webhooks/tableo", "events": [ "booking.created", "booking.updated", "booking.cancelled", "booking.seated" ] }'{
"data": {
"id": 1,
"url": "https://api.example.com/webhooks/tableo",
"secret": "whsec_a1b2c3d4e5f6...",
"events": [
"booking.created",
"booking.updated",
"booking.cancelled",
"booking.seated"
],
"is_active": true,
"created_at": "2026-06-01T10:00:00Z",
"updated_at": "2026-06-01T10:00:00Z"
}
}{
"error": {
"code": "string",
"message": "string",
"details": {}
},
"meta": {
"timestamp": "2019-08-24T14:15:22Z"
}
}{
"message": "The given data was invalid.",
"errors": {
"date": [
"The date field is required."
],
"adults": [
"The adults must be at least 1."
]
}
}