Shared

Dates that do not follow the recurring week

get/api/organization/special-hours

Special hours (book_special_hours, migration 0109): what a business does on ONE date, instead of the weekly hours or service periods that otherwise repeat forever.

Both engines, unlike /api/organization/hours. "Closed on Christmas Day" and "open 10:00 to 14:00 on Boxing Day" are sentences a salon and a restaurant both need, and each engine composes them with its own maths: for appointments the windows replace book_company_hours for that date, for hospitality they clamp that date's book_service_periods (which keep their own name, turn time and covers cap) and extend them where the windows reach past anything the venue has defined.

Returns dates from the business's own today onward, in date order, capped at 400. from overrides the start. today comes back alongside, computed in book_companies.timezone rather than the server's, so a caller does not have to guess which day the venue is on. Readable by any member; only the writes are gated.

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

Query Parameters

from?string

YYYY-MM-DD. Defaults to the business's own today.

Formatdate

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/api/organization/special-hours"
{  "today": "2019-08-24",  "dates": [    {      "special_date": "2019-08-24",      "windows": [        {          "startTime": "string",          "endTime": "string"        }      ],      "label": "string",      "holiday_name": "string"    }  ]}

Publish the Booking Page settings draft live (admin) POST

Copies `draft_content` onto the real `book_companies` columns it mirrors and stamps `booking_page_published_at`. Unconditionally admin-only, with no `manage_org_settings` escape hatch, the same posture POST /api/site-pages/{type}/publish takes and for the identical reason: a staff caller who can edit a draft must still not be able to make it live. No re-validation here on purpose, since PATCH /api/booking-page-draft already validated every value on the way in, so this route trusts what it reads back out. A company that never touched this screen since the draft system shipped gets a clean 200 no-op: `book_companies` already holds whatever was last saved the old, still-live instant-save way, and there is nothing to copy forward.

Set what happens on one date (admin, or manage_org_settings) POST

An upsert keyed on `(company_id, special_date)`, so sending the same body twice is the same as sending it once. **An empty `windows` array means CLOSED, and it is the most common write this route takes.** That is the opposite of what an empty `book_company_hours` means, and the two live inches apart: zero rows there means UNCLAMPED (every org is in that state until it saves hours), while here the row EXISTS, so `[]` is somebody deliberately saying "nothing". Closing a date is therefore NOT the same as `DELETE`ing it, which returns the date to the ordinary week. An `endTime` earlier than `startTime` runs past midnight (migration 0042) and is accepted: an 18:00 to 02:00 New Year's Eve is the single most likely row anyone writes here. Only a zero-length window is refused. No replace-the-set function stands behind this, unlike the hours and service-period routes: one date is one row, so an upsert is already one statement and therefore one transaction. Gated to admin, or a staff login granted `manage_org_settings` (migration 0082). **Conflicts** against existing, non-cancelled bookings on the date are checked live, unconditionally, the same `findConflictingAppointments` predicate `POST /api/providers/{id}/availability-overrides` already uses: does the booking fall inside one of the proposed windows. Reads `book_reservations` for a hospitality company and `book_appointments` for an appointments one, company-wide rather than per-provider, since this table has no provider column. A conflict refuses with 409 and nothing is written; resend with `conflictAcknowledged: true` to proceed anyway. Unlike the override route this never hard-blocks by role: every caller who can reach this route already holds `manage_org_settings`, so acknowledgment alone is the gate. The route never touches the conflicting bookings themselves: no auto-move, no auto-cancel, no auto-refund. Resolving each one is a separate, deliberate action from Bookings.