Join the waitlist (public)
/api/book/{slug}/waitlistWhat a guest leaves behind when a date came back with no tables at all (migration 0044). Creates a book_waitlist_entries row with status open, which lands on the venue's reservations screen.
Until this existed, a fully-booked Friday was a dead end; the widget said "try another day" and the guest left, taking with them the one signal a booking page most wants. Nothing is held or promised by joining. A table is offered later by a member pressing a button, and the guest re-books through the ordinary flow; whoever gets there first gets it.
Same obligations as every other public write here: guestRateLimit on the write budget runs FIRST, before the slug is resolved and before the body is parsed, so this cannot be used to probe which slugs exist. A party ABOVE the venue's maxOnlinePartySize is refused with enquiryRequired; above that line the venue has said it wants to look at the group, and a waitlist entry is a promise that a table might simply be offered.
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.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/book/string/waitlist" \ -H "Content-Type: application/json" \ -d '{ "requestedDate": "2019-08-24", "partySize": 1, "name": "string", "email": "string", "phone": "string" }'{ "ok": true, "waitlistId": "83bf03b0-e6bc-406b-a1cd-2e6c45d6197b"}Send a big-group enquiry (public) POST
What a guest sends when their party is over the `maxOnlinePartySize` a venue has set (migration 0036). Creates a `book_group_enquiries` row and opens a thread on it, which lands in the dashboard inbox beside its booking conversations. This is precisely the public unauthenticated write surface migration 0024 declined to build for intake forms; "a standalone shareable form URL would be a new unauthenticated write surface needing its own rate limiting and spam story". It inherits that whole obligation: `guestRateLimit` on the `write` budget runs FIRST, before the slug is even resolved, so this cannot be used to probe which slugs exist either. **A party at or below the ceiling is refused**, and a venue that has set no ceiling at all refuses everything here. Without that check this endpoint would be an open "email the business" box on every hospitality org.
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}, ...}}`.