Public booking (hospitality)

Confirm a held table (guest checkout, step 2)

patch/api/book/{slug}/reservations/{id}

Attaches the guest's details and promotes the hold to confirmed. The update is a compare-and-set on status = hold, so two racing confirms cannot both succeed; the loser gets 410 and, importantly, does not send a "table confirmed" email for a booking it did not make.

hold_expires_at is cleared in the same statement because the check constraint is biconditional; setting status alone would fail.

Path Parameters

slug*string

The organization's public booking slug, i.e. the {slug} in /{slug}. Only orgs with status active resolve; anything else is a 404.

id*string

The reservationId returned by the hold. Scoped to the slug, so one venue's URL can never confirm another venue's hold.

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

application/json

curl -X PATCH "https://example.com/api/book/string/reservations/string" \  -H "Content-Type: application/json" \  -d '{    "customer": {      "name": "string",      "email": "user@example.com",      "phone": "string"    }  }'
{  "reservation": {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "starts_at": "2019-08-24T14:15:22Z",    "ends_at": "2019-08-24T14:15:22Z",    "party_size": 0  }}

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.

Release a held table DELETE

The guest closed the sheet or hit back. Releases the table immediately rather than waiting out the hold window. Only ever deletes a row whose status is still `hold`; a confirmed booking is cancelled from the dashboard or the manage link, never dropped by an unauthenticated caller holding an id.