Switch between the subdomain and the classic /{slug} link as the asserted address (admin)
/api/organization/website/subdomain-preferenceFlips book_companies.site_subdomain_preferred. Refuses with 400 rather than a silent no-op if the company has no subdomain provisioned yet: there is nothing this preference could mean before then, and the dashboard control itself is never shown in that state. Not vertical-gated, unlike most of the Website screen's own routes: hospitality can hold a subdomain too (ensureTenantSubdomainIfEntitled, tenant-subdomain.ts, fires from the Stripe webhook regardless of business_type), so this has to be reachable from both engines. Admin-only, same posture the sibling offline route takes: this changes what search engines and the dashboard itself treat as this business's real address, a bigger call than the general settings a staff member with manage_org_settings can already make. No cache to invalidate, unlike offline's own route: tenantCanonicalUrl() reads this column with a plain, uncached read on every call, by design, so there is nothing stale to revalidate.
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
curl -X POST "https://example.com/api/organization/website/subdomain-preference" \ -H "Content-Type: application/json" \ -d '{ "preferred": true }'{ "ok": true}Public holidays for this business's country and state GET
Suggestions for the Special hours screen, proxied from [date.nager.at](https://date.nager.at) and narrowed to the company's own `country` and `state`. **Read-only, and it writes nothing.** It answers "which dates does your country and state observe"; what the business then does on those dates is a separate, deliberate `POST /api/organization/special-hours`. Keeping the two apart is what stops a third-party dataset from ever changing a tenant's availability on its own. **Regional filtering is not cosmetic.** In 2026 the source returns Anzac Day twice, on 25 April for SA/TAS/VIC and 27 April for NSW/ACT/WA. Unfiltered, a Sydney venue is shown two Anzac Days and one of them is a day it is open. When the company's state cannot be resolved to an ISO 3166-2 code, everything is returned and `regionUnknown` says so, because hiding a holiday we could not verify is the worse failure of the two. Only `Public` holidays are offered; the source also reports Bank, School, Optional, Observance and Authorities days, and a list that offers to close a salon for Groundhog Day is a list nobody trusts again. **Never 5xx on a source failure.** A country the source does not cover, a missing country setting and an upstream outage all answer 200 with `supported: false` and a `reason`, because the Special hours screen works perfectly well without suggestions and rendering an error over a working feature teaches operators to distrust it. Cached for a week per country-year. Readable by any member.
Every business this account can act in GET
The switcher's list. Returns one entry per `book_org_members` row the caller holds, so an account with a single business gets a list of one and the switcher renders nothing. **The roster IS the list, deliberately.** `POST /api/organization/switch-business` authorizes against the same table, so what is offered here and what is permitted there cannot drift apart. An organization owner appears on each of their businesses because they hold an ordinary `admin` row on each: this route does not know organizations exist. Not gated with `requireMember()`, and read with service-role, for the reasons switch-business sets out: `requireMember()` answers "is the caller in the company they are currently in", which is the wrong question for a list that spans all of them, and both `book_companies` and `book_org_members` are scoped by the `company_id` claim, so an RLS-scoped read would return a list of length 1 for everybody. The query is pinned to the caller's own verified `user.id`.