Guest self-service

Retired email logo: redirects to the G7 mark

GET
/api/email-assets/logo

Emails sent before 2026-09-28 embed the Gaplessly logo from this URL, which used to draw the pre-G7 mark per request, tinted with the org's accent (the accent, crescent and h query parameters). Emails now carry the G7 logo as static files under /email-icons/ (render.ts's logoImg), so this route only keeps those older emails from showing a broken image: every request is a permanent redirect to /email-icons/g7-mark-legacy.png, the G7 mark centred in the old mark's 253:376 shape so the old <img> boxes don't squash it. The query string no longer changes anything. No session, same as every email asset.

Response Body

curl -X GET "https://example.com/api/email-assets/logo"
Empty

Log a guest's thumb-up tap and redirect to the org's Google review link GET

The other branch of the post-visit satisfaction gate: a GET, not a POST, because this is the link inside the confirmation/reminder email itself (`review-email.ts`'s `positiveReviewUrl`); a guest clicks it exactly like any other mailto/href, never sees this route, and is never shown a page belonging to it. Logs a bare click (`rating: 'positive'`, no body, no ratings; migration 0073's own constraint enforces that shape) then redirects straight to Google. Same credential model as the sibling POST route: the manage token alone resolves the booking, no session. **Logging is best-effort and never blocks the redirect.** A DB error or a rate-limit hit is swallowed; the guest always lands on Google. The click log itself exists for a later Google Business Profile connector to match a new public review back to a booking by guest name or time-proximity, not something worth degrading a happy guest's one tap over. Re-opening the link edits the existing row's `created_at` rather than stacking a second one (0073's partial unique index), which also keeps a later time-proximity match pointed at the most recent tap.

Public booking MCP endpoint (JSON-RPC) POST

The Model Context Protocol endpoint an AI assistant connects to in order to read this platform's public booking surface on a guest's behalf. One endpoint for the whole platform, with the venue as a tool argument: both directories that can list an MCP server require it to call the operator's own first-party APIs from a domain matching the service, and no mainstream assistant discovers an MCP server from a web page at all, so a per-tenant endpoint would be unlistable by construction. **Not a REST operation, and the shape of this entry reflects that.** The body is JSON-RPC 2.0 and the method vocabulary is the MCP specification's (`initialize`, `tools/list`, `tools/call`, ...), not anything defined here; an OpenAPI schema for it would describe the transport and say nothing useful about the contract. Read the MCP specification for the envelope, and `tools/list` against this endpoint for the tools, which is the authoritative and self-describing answer. **Five tools, none of which books anything.** `get_venue`, `find_appointment_times` and `find_table_times` read; `book_appointment` and `book_table` turn a slot token into a short-lived link on the venue's own booking page, opened at that time, where the guest enters their own details and confirms. No tool accepts a guest's name, email or phone, none moves money, and none takes a table or provider out of inventory: opening a `book_table` link does cause the booking page to hold that table for about ten minutes, but the tool call itself holds nothing. Everything returned is already served to any anonymous visitor of `/book/{slug}`. **Findable on every plan, bookable from Growth.** `get_venue` answers any venue with its `bookingPage` link. The two time searches and the two booking tools need the venue's plan to include `ai_booking` (Growth and up, src/lib/billing/config.ts); below it they answer with `bookingPage` and `liveTimes: false` instead of times or a link at a time. **No timezone and no machine-readable instant is ever returned.** Times come back as display text plus an opaque HMAC-signed slot token, because a model handed an IANA zone next to a time converts it and books hours wrong. The token carries the real instant and is the only handle a future write tool will accept, which is also what stops a prompt injection moving a booking to another venue while the transcript still shows the right one. **No CORS, deliberately, and none is planned.** An MCP client is a server-to-server HTTP client. The browser-based MCP Inspector cannot reach this and is not meant to; use the Inspector CLI. Authentication: none, and that is the design rather than a gap. This surface holds no credential and reaches no tenant-private data. The separate PRIVATE per-tenant MCP server, which is not built, binds one `sk_live_` key to one company and never accepts a venue as a tool argument; the two trust models are deliberately not blended.