Appointments engine

Approve a pending availability-override request

post/api/availability-overrides/{id}/approve

Admin only, mirroring POST /api/waitlist/{id}/offer's shape (a special action gets its own route rather than being a plain status PATCH). Re-runs the conflict check LIVE rather than trusting anything the client sends; a booking made after the request was submitted, or after the admin last looked at the list, must not be silently approved over.

Reassignment is not a parameter here. The dashboard calls the existing, unmodified PATCH /api/appointments/{id} for each conflicting booking it wants to move, sequentially, BEFORE calling this route, never one endpoint doing both, so a failed reassignment cannot leave the override half-decided.

Notifies the submitting staff member's linked login on success.

Authorization

sessionCookie
sb-qxrvgfkjyvbngipqvslu-auth-token<token>

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

id*string

Availability-override request id.

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 POST "https://example.com/api/availability-overrides/string/approve" \  -H "Content-Type: application/json" \  -d '{}'
{  "ok": true}

Cancel or reject an availability-override request PATCH

Two plain status transitions on a `pending` row: `cancelled` (the staff member who owns the linked provider, on their own row, or any admin) and `rejected` (admin only). `approved` is deliberately refused here even though the request shape alone would not stop it; it is a special action (POST .../approve), same rule PATCH /api/waitlist/{id} enforces for `notified`. `superseded` is never client-settable at all. Both transitions call a SECURITY INVOKER database function (`book_cancel_availability_override` / `book_decide_availability_override`, migration 0052) rather than writing the row directly; this table has no update policy for `authenticated` at all, so a raw PostgREST update would be refused the same way from anywhere else. The functions re-check role/ownership themselves from the database; this route's own gates are the ordinary UX path, not the enforcement.

Create an appointment, or a repeat series (staff side) POST

A booking taken by staff; a phone call, a walk-in. Deliberately NOT slot-validated the way guest checkout is: staff have the calendar in front of them, and `book_appointments_no_overlap` (a Postgres exclusion constraint) is the real backstop either way. `price`, `duration_minutes` and `ends_at` are all snapshotted from the chosen service; sending them is not possible and would not be honoured. Status is always `confirmed`. **Repeats (migration 0149).** An optional `repeats` object turns this into a series: every so-many weeks, on one or more weekdays, ending after a number of visits or on a date. When present it REPLACES `startsAt` entirely (`repeats.startsOn` + `repeats.timeOfDay` already say when the first visit is) and the response shape changes; see the 200 below. Deliberately the only creation surface a repeat series gets: not the public booking widget, and not the hospitality engine. `book_appointments_no_overlap` is still what actually prevents double-booking a generated date; a date that conflicts is skipped, not fatal to the rest of the series, matching the shipped feature this follows ("dates that don't fit... can be skipped"). One `series_confirmed` notification job is queued for the whole series, not one `booking_confirmed` per visit: each visit still gets its own reminder for free, since it is an ordinary row the existing reminder cron already reaches, and can still be individually moved or cancelled through this same PATCH/route below exactly like any other booking.