Read one booking
/api/appointments/{id}The single-booking twin of the row builder behind the Bookings list: same embeds, same staff-scope confinement, same redaction. Exists so Calendar can pop the exact same BookingDetailDialog in place on a click, instead of navigating to /dashboard/main/bookings?open=id and stranding the viewer on a different page once they close it.
Staff-scope and redaction both follow docs/staff-privacy.md: a scoped staff member reaching a colleague's booking gets a 403, not a 404, since reads are filtered rather than hidden; and a staff caller's view of the customer is redacted per the org's staffClientVisibility setting, using this booking's own providerConfirmed as the anonymized-mode grant.
Authorization
sessionCookie The dashboard's Supabase Auth session cookie, set at sign-in. Large sessions are split across
numbered chunks (…auth-token.0, .1), so treat this as a cookie family rather than one name.
Every request re-validates it against the Auth server (getUser()), never by decoding the cookie
locally: a JWT nothing has checked is not a credential. Tenancy is then read from the verified
app_metadata.company_id claim and enforced by row-level security; it is never read from request
input, on any route, ever.
role (admin / staff) is deliberately not in RLS. It gates specific actions in route code,
the operations marked admin below, so hiding a button in the UI is cosmetic only, and a route's
own check is the enforcement.
In: cookie
Path Parameters
Appointment id.
Response Body
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/api/appointments/string"{ "row": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "startsAt": "2019-08-24T14:15:22Z", "endsAt": "2019-08-24T14:15:22Z", "status": "pending", "price": 0, "notes": "string", "serviceId": "8f8bb40f-b96b-40fe-9064-5031fbe483f9", "providerId": "4834bcdc-4a64-444d-966b-1a6fe381da24", "durationMinutes": 0, "depositStatus": "none", "unreadCount": 0, "serviceName": "string", "itemNames": [ "string" ], "serviceColor": "string", "providerName": "string", "providerConfirmed": true, "customerName": "string", "customerEmail": "string", "customerPhone": "string", "customerTags": [ "string" ] }}End a series early PATCH
"End a series early from any of its visits: the rest cancel in one go and the customer hears once" (the shipped spec this follows). Only `{"status":"cancelled"}` is accepted: there is no other transition a series makes. Cancels every visit in the series that is both in the future AND not already `cancelled`/`completed`; a visit that already happened, or was cancelled individually by the guest or staff before this ran, is left exactly as it was rather than double-touched. `book_booking_series.status` flips to `cancelled` in the same action, which is what refuses a second call on an already-ended series with 409. One `series_cancelled` notification job is queued for the whole batch, not one per visit.
Update an appointment PATCH
A true partial update: only keys actually present in the body are written, so the drawer's `{status}`-only call cannot blank a field it never mentioned. Derived fields, none of which the caller may set: sending `serviceId` re-snapshots `price` and `duration_minutes` from the service actually chosen; `ends_at` is always recomputed from `starts_at` + duration. `phone` is a shortcut to the linked client's record and updates it everywhere that client appears.