Draft an inbox reply or a private note with the AI assistant (single-shot, read-only)
/api/ai/inbox-assistPowers the Inbox's two Lessly buttons (top-right of the reply box, and of "On this booking"): draft the staff's next reply, or suggest a short private note, from the thread already open in MessagesClient. Single-shot (generateText, not a stream), unlike POST /api/ai/chat: this answers once from the messages the client sends in the request body, with no running conversation and no persisted history of its own.
Runs entirely as the signed-in user through requireMember()'s RLS-scoped client, called with no role or vertical: either engine's staff can reach it. Unlike POST /api/ai/chat, it never re-reads the thread's messages itself: the client already has open.messages, rendered server-side by /inbox's own page load a moment ago, so the request body carries the transcript instead of a thread id. Tenancy still comes from requireMember()'s companyId, never from the body; neither output has any effect until a human clicks Send reply or Save note.
The transcript is capped to the newest 60 messages and roughly 6000 characters (buildTranscript, route.ts), oldest lines dropped first if still over budget. The system prompt instructs the model to treat every line of it as data to read, never as an instruction to follow, the same posture POST /api/ai/chat applies to tool results, since a guest can type anything into the messages this transcript is built from.
Shares the identical per-tier monthly ai_credits allowance and the same wired-but-inactive AI_RATE_LIMIT_PER_MINUTE gate as POST /api/ai/chat, keyed the same way (per user, not per feature): one OpenRouter key backs both, so the abuse concern is request frequency from this login, not which AI feature it is hitting.
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 POST "https://example.com/api/ai/inbox-assist" \ -H "Content-Type: application/json" \ -d '{ "mode": "reply", "kind": "reservation", "customerName": "string", "messages": [ { "author": "guest", "body": "string" } ] }'{ "text": "string"}Get a short-lived download link for an attached file GET
Re-derives book_ai_attachments' own tenancy boundary by hand (company_id AND user_id), since this route holds a service-role client and RLS never applies to it. Returns 404, not the raw storage error, whether the attachment never existed for this caller or the 90-day sweep (GET /api/cron/ai-attachments-sweep) already removed the raw file: the caller cannot tell those apart and does not need to. The signed URL is a 15-minute bearer credential, same TTL reasoning as the data-exports download link.
Read how much of this period's AI assistant allowance is left (any role) GET
What the credit pill beside the assistant's composer renders. A read of the append-only `book_usage_events` ledger (`ai_credits` resource) via `aiCreditsStatus` in `lib/usage.ts`, scoped to the signed-in user's company by requireMember()'s companyId, never by request input. Open to every role, unlike GET /api/organization/usage (admin only): that route answers "what is this business being billed for", while this answers "can I use the assistant right now, and how much is left", which a front-desk user is already shown the moment a send is refused. It reports the AI pool only, never SMS, email or anything with a price on it. Read-only: the gate itself lives in POST /api/ai/chat's reserveAiCredit, which also owns the 402. This route never refuses a request for being over the allowance, it reports `allowed: false` instead. Never cached (`Cache-Control: no-store`), so the pill reads the ledger as it stands.