Shared

Update a client

patch/api/clients/{id}

Partial: only present keys are written.

Email is deliberately not editable. It is the client identity (the unique index on company_id, email), so changing it is a merge or a split of two records and their histories, not a field edit. Sending email is silently ignored.

tags and notes are both the venue's PRIVATE data about the client. Neither is ever rendered on a guest-facing surface or included in any email or SMS; the notify payload builders name their columns explicitly so they cannot start doing so by accident.

marketingStatus (migration 0143) is one-directional. The only accepted value is 'unsubscribed'; any other value, including 'subscribed', is a 400. Consent is the client's to give, never staff's to restore on their behalf, so this route has no way to resubscribe someone.

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

Path Parameters

id*string

Client id.

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Properties1 <= properties

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X PATCH "https://example.com/api/clients/string" \  -H "Content-Type: application/json" \  -d '{}'
{  "ok": true}

Create a client POST

A client with no booking behind it yet. Shares its find-or-create-by-email path with the two staff-side create routes, and reports which branch it took so the UI can say "you have merged into an existing record" instead of silently touching someone else's. With no email there is no dedup key at all, so a fresh row is always created. That is an accepted tradeoff, not a gap: `book_customers` allows a null email and the unique index is total.

Erase a client's personal data on request (admin) POST

Closes the gap the privacy policy already names: a business is responsible for honouring its own customers' access/deletion requests, but no tool existed to actually fulfil one for a single guest; only DELETE /api/account (the whole workspace) did. Anonymises in place; never deletes the row. `book_appointments.customer_id`/`book_reservations.customer_id` are both `NOT NULL` with no `ON DELETE` clause, so a client with any booking history cannot be hard-deleted without also deleting that booking, which an erasure request never asked for. Clears `name` (replaced with "Erased guest"), `email`, `phone`, `notes`, `tags` and the birthday fields, and sets `marketingStatus` to `'unsubscribed'` permanently. Booking history, `id`, `company_id` and `created_at` are untouched. Never refuses on upcoming bookings: the count is reported instead, since refusing would let a venue stall a legitimate request indefinitely just by leaving one on the calendar. Writes an append-only `book_compliance_events` row (`customer_erased`) recording who did it and when. Idempotent: re-running this on an already-erased record just rewrites the same values and logs another event.