Shared

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

post/api/organization/special-hours

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 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
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

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"  }}