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
| Event | When |
|---|---|
order.created | A new order is placed via API, POS, QR or online. |
order.updated | Body, items or status change (non-terminal). |
order.completed | Order moves to completed. |
order.cancelled | Order moves to cancelled. |
reservation.created | New reservation (any channel). |
reservation.updated | Time, party size, table reassignment, etc. |
reservation.cancelled | Status flips to cancelled. |
reservation.no_show | Marked 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.