Tableo Restaurant API
Webhooks

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 as id in 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 webhook secret.
  • Tableo-Signature (recommended, anti-replay): Stripe-style header t=<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 payload id).
  • 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-Id for best-effort ordering within a single webhook.

Events

See the Events reference for the full list of event types and when each one fires.

POST
/api/restaurant/webhooks

Authorization

bearerAuth
AuthorizationBearer <token>

Supports two token types:

  1. Restaurant tokens — API tokens with restaurant-api ability, issued per restaurant.
  2. User tokens — Personal access tokens with mobile-access ability (e.g. for the mobile app). When managing multiple restaurants, include X-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."
    ]
  }
}