Update organization settings (admin, or manage_org_settings, or deposit_staff_editable)
/api/companiesA true partial update: a key that is not in the body is not in the UPDATE.
This paragraph used to say the opposite, and the opposite used to be true; every field was derived with a fallback, so omitting tagline nulled it, omitting notifyEmailEnabled forced it true, and omitting bookingTheme reset it to light. That was fixed in the route (a has() gate, the same discipline the reservations, appointments and clients PATCH routes use) and the description was not moved with it. scripts/check-hardening.ts asserts the current behaviour column by column.
Three ways in (migration 0082), checked in order of breadth. An admin may write anything below. A staff login granted manage_org_settings may write anything EXCEPT five admin-only keys that either change the org's public identity (slug, status) or grant a permission (depositStaffEditable, unlinkedStaffFullAccess, staffClientVisibility); a permission that can grant permissions is not delegable, it is the thing doing the delegating. Absent that, a staff login may still write the six deposit-policy fields alone, but only once this org's admin has turned on depositStaffEditable (migration 0075).
slug, status and the three admin-only permission flags are written with service-role, because migration 0022 (and 0075 for the deposit flag) leaves those columns out of the tenant column grant; a staff JWT provably could flip them through raw PostgREST before that. Everything else goes through the RLS-scoped client.
business_type is NOT settable. Flipping a live org's vertical strands whatever it already has; a conversion is a data migration, not a settings toggle.
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
application/json
curl -X PATCH "https://example.com/api/companies" \ -H "Content-Type: application/json" \ -d '{ "name": "string", "slug": "string", "status": "active" }'{ "ok": true}Confirm a newsletter sign-up (public) POST
The confirmation page's button: flips the Resend contact named by `id` (from the emailed link) to subscribed. Form-encoded, answered with a 303 back to `/newsletter/confirm` (`?status=done`, or `?id=...&status=failed`), never JSON. POST only; the page GET renders and writes nothing, because mail scanners fetch every link in an email unprompted. The contact id is a random uuid only Resend and the address owner see, so it is the whole credential; anything that is not a uuid is refused without calling Resend. Rate-limited (`newsletter-confirm:` bucket, 10/min-window/IP).
One thread's messages GET
A single booking or enquiry's conversation, for the compact panel inside a booking's Drawer (ReservationsClient/BookingsClient); the full inbox at /inbox assembles every thread server-side in one page load; this is the one-thread equivalent for a client component that only has an id. Same staff-scope as /inbox's own appointment branch (docs/staff-privacy.md): a `kind=appointment` booking outside a scoped staff member's own confirmed provider resolves to nothing; a 404, identical to a booking id from another tenant or one that no longer exists. `reservation` and `enquiry` are never scoped. `senderName`/`senderAvatarUrl` (0057) are present on staff messages only: the specific provider's own identity when the reply is attributed to one, else the company's own; "the business replied."