Shared

Mark a thread read

patch/api/messages

Marks this booking's unread GUEST messages as read. Separate from POST so opening a conversation does not require writing one.

The booking is looked up before the update, and an id that is not this org's is a 404. The number of messages actually flipped is deliberately NOT reflected in the status: a thread of your own with nothing currently unread matches zero rows and is still a 200, because marking a read thread read is idempotent, not an error.

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

curl -X PATCH "https://example.com/api/messages" \  -H "Content-Type: application/json" \  -d '{    "kind": "reservation",    "bookingId": "9a471128-954e-4e64-bde9-e8147015df89"  }'
{  "ok": true}

Reply to a guest POST

Any member, not admin-only: answering a customer is the job, and gating it behind admin would leave the person actually on shift unable to respond. The `author` value is pinned to `staff` by the RLS WITH CHECK, not merely set here; a crafted request cannot post in a guest's name. After the reply is recorded, `notifyMessageReply()` sends it to the guest on the requested `channel` (email by default); awaited, not fired-and-forgotten, and never throws, so a delivery failure changes nothing about this response. `sms` additionally requires the SMS entitlement (`plan.ts`'s `planAllows(companyId, 'sms')`, i.e. a plan with texts, Growth and Pro (Venue Growth and Venue Pro), and never during a trial) and a phone number that can be resolved to E.164 against the company's country/timezone; either failing means only the log records it, the reply itself still succeeds. **Not `smsIncluded`.** That limit is 0 on every tier since texts became metered, so gating on it refused everyone; the `/inbox` toggle did exactly that until 2026-08-11.

Timestamp of the newest visible message GET

One `created_at`, nothing else: the newest `book_booking_messages` row this login can see (guest message or staff reply alike). `/inbox` (`MessagesClient.tsx`) polls this every 45 seconds while the tab is visible and compares it with the value the page rendered from, calling `router.refresh()` only when they differ. Replaces a blind full-page refresh on the same interval. Same RLS-scoped client as the inbox's own read, so the two agree exactly when nothing is new.