Confirm a held table (guest checkout, step 2)
/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
The organization's public booking slug, i.e. the {slug} in /{slug}. Only orgs with status active resolve; anything else is a 404.
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.