Guides
Restaurant table booking
Let guests pick a date, time and table on a visual floor plan of your venue — embedded on your site or driven headlessly through the API.
How it works
A room (Main floor, Terrace…) is a resource of type table. Its tables are named units with a position on the floor plan, a shape and a min/max party size. Tables can be joined into combinations for larger parties. Each room has booking rules (arrival interval, seating duration by party size, buffer, deposit, no-show handling) and opening hours (one or more service windows per weekday).
A booking occupies one or more tables for a time window. Overlaps are prevented by a database constraint, so double bookings cannot happen even under concurrent load.
Set up in the dashboard
Create a room
Opening hours & rules
Create a table widget
Embed
The iframe snippet includes a tiny listener so the widget grows and shrinks with its content on phones:
<iframe id="zentier-tables"
src="https://YOUR_APP_URL/embed/TENANT_UUID?key=zentier_pk_xxx&widget=WIDGET_UUID"
width="100%" height="720" frameborder="0" style="border:none;border-radius:12px"></iframe>
<script>
window.addEventListener('message', function (e) {
if (e.origin !== 'https://YOUR_APP_URL') return;
var d = e.data || {};
if (d.type === 'booking-widget-event' && d.event === 'resize' && d.height) {
document.getElementById('zentier-tables').style.height = d.height + 'px';
}
});
</script>With the SDK, auto-height is built in. Prefill the party size, date or room with data attributes:
<script src="https://YOUR_APP_URL/sdk/booking-widget.js"
data-tenant="TENANT_UUID" data-key="zentier_pk_xxx" data-widget-config="WIDGET_UUID"
data-api-url="https://YOUR_APP_URL/api" data-embed-url="https://YOUR_APP_URL"
data-mode="inline" data-target="zentier-tables"
data-party-size="2" data-date="2026-10-04" data-room="ROOM_RESOURCE_UUID"></script>
<div id="zentier-tables"></div>BookingWidget.on('tableSelected', ({ unit_ids, names }) => console.log(names))
BookingWidget.on('bookingSuccess', (booking) => console.log(booking.tables))
BookingWidget.on('resize', ({ height }) => {})
// Open the modal prefilled
BookingWidget.openTableBooking({ partySize: 4, date: '2026-10-04' })
// Headless
const day = await BookingWidget.checkTableAvailability({ roomId, date: '2026-10-04', partySize: 4 })
const result = await BookingWidget.bookTable({
roomId, startTime: day.times[0].start_utc, partySize: 4,
customerName: 'Anna', customerEmail: 'anna@example.com',
})API
/api/widget/configPublishable keyfloor_plans: canvas, decor elements, tables (position, shape, seats), combinations, booking rules and service windows./api/widget/tables/availabilityPublishable key?room_id&date&party_size → arrival-time grid with available | limited | full per time. Add &start_time= for per-table states at that seating. ?mode=dates&start_date&end_date → open | full | closed per date./api/widget/tables/holdPublishable key{ room_id, unit_ids, start_time, party_size, hold_client_id }./api/widget/tables/bookingPublishable key{ room_id, start_time, party_size, unit_ids | null, customer_name, customer_email, … }. Free bookings return is_free: true; deposits return a Stripe/Mollie checkout. A 409 with TABLE_NO_LONGER_AVAILABLE includes alternative times./api/bookings/{'{id}'}/tablesDashboard sessionWebhooks
In addition to booking.created / booking.paid / booking.cancelled, table bookings emit booking.seated, booking.no_show, booking.completed and booking.table_assigned. See Webhooks.
Time zones
start_utc) and the local label (time_local) so you never need to convert on the client.