Log a guest's thumb-up tap and redirect to the org's Google review link
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.
Path Parameters
The booking's manage token, reused as the feedback link's credential; see feedbackUrl() in lib/booking/manage.ts.
Query Parameters
Star-mode only (review_ask_style = 'stars'): the exact rating the guest tapped, carried by review-email.ts's high-rating links and stored as overall_rating on the positive row (20260921's widened constraint). Clamped, not trusted: anything that is not an integer within [the org's review_star_threshold, 5] is dropped and the tap logs as a bare click, so a hand-edited URL cannot make a positive row claim a rating that would never have reached this branch. Absent in thumbs mode.
3 <= value <= 5Response Body
curl -X GET "https://example.com/api/feedback/string/positive"Set or clear a customer's marketing consent from the link in a marketing email POST
The write half of the unsubscribe surface (migration 0143); the page at /unsubscribe/{token} only renders, this route is the only thing that mutates. POST only, form-encoded, never JSON: read via `request.formData()`, which parses both multipart/form-data and application/x-www-form-urlencoded transparently. That matters for RFC 8058: a mail client rendering a native "Unsubscribe" button POSTs exactly `List-Unsubscribe=One-Click` as application/x-www-form-urlencoded, and this route reads it with the same parser the guest-facing form on the page uses, so there is one code path for both a mail client's automated click and a guest's real one. `token` is `book_customers.unsubscribe_token`, a per-customer-per-company credential (not global): a person who books at two venues has two separate tokens and unsubscribing at one never touches the other. No session; the token alone resolves who this is, same posture as /api/feedback/{token}. An unrecognised or empty body defaults to `unsubscribe`, never `resubscribe`: fail-safe toward less contact, not more. A `stopSms=1` field alongside an unsubscribe additionally sets `sms_opt_out_at`; it has no effect on a resubscribe, since SMS consent is its own axis and a resubscribe must never silently reinstate it.
Retired email logo: redirects to the G7 mark GET
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.