Mark a thread read
/api/messagesMarks 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 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.