Saltar al contenido

Webhooks

Listen for restaurant lifecycle events.

Tablezio POSTs signed JSON to your URL whenever something changes — an order is placed, a reservation is cancelled, stock runs low. Webhooks are the right way to react in real time.

Register an endpoint

Create a webhook endpoint via the API or the dashboard. The response includes a signing secret — store it now, it won't be shown again.

POST /v1/webhook_endpoints
{
  "url": "https://example.com/webhooks/tablezio",
  "enabled_events": ["order.created", "order.cancelled", "reservation.created"],
  "description": "Production handler"
}

→ 201 Created
{
  "id": "we_…",
  "object": "webhook_endpoint",
  "url": "https://example.com/webhooks/tablezio",
  "enabled_events": ["order.created", "order.cancelled", "reservation.created"],
  "signing_secret": "whsec_8b2c3d4e5f6a…",
  "is_active": true
}

Event payload

Every event has the same envelope. The full resource at the time of the event is in data.object.

POST https://example.com/webhooks/tablezio
{
  "id": "evt_8b2c3d4e5f6a7b8c9d0e1f20",
  "object": "event",
  "type": "order.created",
  "created": 1716730800,
  "livemode": true,
  "data": {
    "object": {
      "id": "ord_…",
      "object": "order",
      "order_number": "ORD-042",
      "type": "takeout",
      "status": "draft",
      "total": 4290,
      "currency": "usd"
    }
  }
}

Verify the signature

Every delivery carries a Tablezio-Signature header in the format t=<unix>,v1=<hex-hmac>. The HMAC input is {t}.{raw-body} using SHA-256 with your endpoint's secret.

Reject any request older than 5 minutes; that protects against replay.

import { createHmac, timingSafeEqual } from 'crypto';
import express from 'express';

const app = express();

// IMPORTANT: read the raw body. Express's JSON middleware mutates it.
app.post(
  '/webhooks/tablezio',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('Tablezio-Signature') ?? '';
    const parts = Object.fromEntries(
      signature.split(',').map(p => p.split('=')),
    );
    const t = Number(parts.t);
    const provided = parts.v1;
    if (!t || !provided) return res.sendStatus(400);

    if (Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);

    const expected = createHmac('sha256', process.env.WHSEC)
      .update(`${t}.${req.body.toString('utf8')}`)
      .digest('hex');
    const a = Buffer.from(expected, 'hex');
    const b = Buffer.from(provided, 'hex');
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
      return res.sendStatus(400);
    }

    const event = JSON.parse(req.body.toString('utf8'));
    // Respond fast, process async.
    res.sendStatus(200);
    handleEvent(event).catch(console.error);
  },
);

Retries and delivery

  • We retry up to 5 times for any non-2xx response or network failure.
  • Backoff is exponential: 30s, 60s, 120s, 240s, 480s.
  • Respond within 10 seconds with a 2xx — even 200 with no body. Process async if you need more time.
  • Every attempt is recorded on the events resource so you can inspect what happened.

Event types

EventWhen
order.createdA new order is placed via API, POS, QR or online.
order.updatedBody, items or status change (non-terminal).
order.completedOrder moves to completed.
order.cancelledOrder moves to cancelled.
reservation.createdNew reservation (any channel).
reservation.updatedTime, party size, table reassignment, etc.
reservation.cancelledStatus flips to cancelled.
reservation.no_showMarked as no-show.

More events will be added — subscribe to * if you want everything as it lands.

Best practices

  • Always verify the signature. Anyone can forge an HTTP request.
  • Use the id on the event to deduplicate — Tablezio may retry on uncertain delivery.
  • Make your handler idempotent. Treat the event as the truth, not your local state.
  • Return 2xx fast; queue heavy work to a background job.