Appointments engine

Take the public site offline, or bring it back (admin)

post/api/organization/website/offline

Flips book_companies.site_offline. Unconditionally admin-only, no manage_org_settings escape hatch, same posture the publish route (/api/site-pages/{type}/publish) takes: this makes or stops making the tenant's public site unreachable for every visitor, a bigger call than the general settings a staff member with that grant can already make. Appointments only, same as every other route under the Website screen: the page itself already redirects a hospitality org away before this control could ever render. Revalidates the public site's cache tag on success, keyed off the company's slug (fetched via the same .select('slug') the update itself performs, not a separate read).

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

curl -X POST "https://example.com/api/organization/website/offline" \  -H "Content-Type: application/json" \  -d '{    "offline": true  }'
{  "ok": true}

This website's traffic (self-hosted, cookieless) GET

The "Your website" card's one data source: a self-hosted Umami instance (analytics.solvintia.com), one Umami "website" per company for clean per-tenant isolation (no cross-tenant prefix-summing to get wrong). Cookieless by design, see the Cookie Policy entry this shipped alongside for the exact claim. Lazily provisions the company's Umami website on first call if it has none yet (ensureUmamiWebsiteId, src/lib/booking/umami-website-admin.ts) rather than requiring a separate setup step. Included on Pro (src/lib/plan.ts, `analytics`), checked before any Umami call; readable by any member once past that gate, since this is a display of the business's own public traffic, not a setting.

List reservations (API key) GET

The hospitality engine's table reservations. The mirror of `GET /api/v1/appointments` (same envelope, same paging, same filters, different table), and deliberately **not** the same endpoint with a flag. The two engines share plumbing and never booking logic (migration 0011), and a single `/bookings` endpoint would be the first place that line got crossed. Engine-guarded: an appointments organization's key is refused with `wrong_vertical` before scope is even checked. Ordered newest first; use `from` and `to` for a service, a day or a week. `holdExpiresAt` IS exposed, unlike the other withheld columns: a `hold` row whose hold has expired is not a booking, and a program reading reservations has to be able to tell. **Three clocks in one object, and they are not interchangeable.** `startsAt` and `endsAt` are the venue's wall clock wearing a `+00` suffix; `holdExpiresAt` and `createdAt` are true UTC instants. All four serialise identically, so nothing in the payload distinguishes them and a caller that parses them alike is wrong about some of them by the venue's entire UTC offset. The sharpest edge is comparing `holdExpiresAt` against `startsAt`, or evaluating it in venue-local terms: it is the one field here whose whole purpose is a comparison against the current real instant. `from` and `to` filter on the same wall clock `startsAt` does. Resolve the venue's zone from `timezone` on `GET /api/v1/organization`, which needs the separate `organization:read` scope. See the timestamps section of `docs/api-keys.md`. Three columns are withheld: `manage_token` (the entire credential for the guest self-service link, handing it to a program hands over the ability to act as the guest), `internal_note` (the venue's private note, excluded for the same reason `book_customers.notes` is), and `stripe_payment_intent_id` (an identifier in somebody else's system).