List which tables are free for one exact slot
The scoped, opt-in reversal of the availability route's "no tableId" stance (migration 0030): given a slot the guest already has from the ordinary grid, lists the tables actually free at that instant, with name and seat count, never coordinates, since none exist in this schema.
Returns 404, not 403, when tablePickerEnabled is off. Checked here at request time against the real row, not trusted from a widget's cached config; an admin who just switched it off must not have a stale tab keep listing table names. This is the actual security boundary for the reversal; the widget's own gate on rendering the sub-step is a courtesy on top of it, not the guard itself.
An instant with no matching slot (outside every period, no table fits, or inside a closure) is not an error; it returns an empty list, the same graceful floor a filtered-out area produces on the main grid.
Path Parameters
The organization's public booking slug, i.e. the {slug} in /{slug}. Only orgs with status active resolve; anything else is a 404.
Query Parameters
YYYY-MM-DD, strictly.
date1 <= value <= 30The exact slot start the guest picked from the availability grid.
date-timeSame filter as the availability route. Mutually exclusive with level_id.
uuidSame filter as the availability route, inclusive of nested rooms. Mutually exclusive with area_id.
uuidSame param the availability and hold-creation requests already send (migration 0207, R1 duration parity): narrows the slot to this published Experience's own explicit hours/seating length instead of the venue's ordinary service periods, so table options agree with the grid that offered the slot and the hold the guest is about to take. Not required; an ordinary reservation has nothing extra to send.
uuidResponse Body
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/api/book/string/reservations/table-options?date=2019-08-24&party_size=1&starts_at=2019-08-24T14%3A15%3A22Z"{ "tables": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "string", "seatsMin": 0, "seatsMax": 0 } ]}List bookable table slots GET
The hospitality availability grid. Expired holds for this org are swept before reading, which makes the system self-healing: an exclusion-constraint predicate cannot call `now()`, so a dead hold would otherwise keep blocking its table until something deleted it. Live holds are counted as busy on purpose; a hold is exactly as blocking as a confirmed booking until it expires. `tableId` is deliberately NOT returned. It would leak the floor plan, and a client-held allocation goes stale the moment someone else books; the POST re-runs the allocator server-side regardless. `GET .../table-options` is the one scoped, opt-in exception, for orgs with `tablePickerEnabled` on. `area_id`/`level_id` (migration 0030) narrow the candidate tables before allocating; passing a `level_id` INCLUDES every area nested under it, not just tables sitting directly on the level with no room, so a fully-subdivided floor is not a location choice that always resolves to nothing. **Ranged mode (`days`).** Same contract as the appointments twin, and the same reason: the wizard's landing walk used to fire one request per day (up to 14), each paying the full fan-out and a hold-sweep DELETE. The single-date shape (`{slots, reason}`) is unchanged and stays the default; sending `days` greater than 1 switches the response to `{days: {"<date>": {slots, reason}, ...}}`.
Hold a table (guest checkout, step 1) POST
Takes a 10-minute hold on a table and returns its id. The guest then confirms it with PATCH once they have typed their details; that gap is exactly what the hold protects. The slot is re-derived server-side and matched exactly against the requested `startsAt`. The exclusion constraint, not the availability read, is what actually makes the allocation safe. **400 versus 409.** The two are not interchangeable and this route distinguishes them: 400 means the request can never succeed (that time is outside every service period, no table seats a party that size, or the venue is closed), 409 means it was bookable and someone else has it. When the allocator comes back empty it is re-run against an empty venue to decide which. The appointments checkout draws the same line, and the widget relies on it; it re-loads the availability grid on 409 only. `areaId`/`levelId` (migration 0030) narrow the candidate tables the same way the availability route does. `tableId` requests a specific one of them; honoured only if `tablePickerEnabled` is on for this org (silently ignored, not rejected, otherwise, so a stale client that cached the toggle a moment before an admin disabled it still completes a normal auto-pick booking) and only if that table is still in the candidate set for this EXACT slot; losing that race is the same 409 family as every other "someone else got there first" outcome here, not a 400; the time itself is still bookable, just not with that one table.