Set what happens on one date (admin, or manage_org_settings)
/api/organization/special-hoursAn 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 DELETEing 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.
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
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/organization/special-hours" \ -H "Content-Type: application/json" \ -d '{ "date": "2019-08-24" }'{ "ok": true, "date": { "special_date": "2019-08-24", "windows": [ { "startTime": "string", "endTime": "string" } ], "label": "string", "holiday_name": "string" }}Dates that do not follow the recurring week GET
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.
Return a date to the recurring week (admin, or manage_org_settings) DELETE
Removes the row, so the date follows the ordinary weekly hours again. **Emphatically not the same as saving it with no windows**, which is how a business says it is shut. Deleting a date that was never set is not an error: the caller asked for this date to follow the ordinary week, and it now does.