Shared

Confirm a newsletter sign-up (public)

post/api/newsletter/confirm

The confirmation page's button: flips the Resend contact named by id (from the emailed link) to subscribed. Form-encoded, answered with a 303 back to /newsletter/confirm (?status=done, or ?id=...&status=failed), never JSON. POST only; the page GET renders and writes nothing, because mail scanners fetch every link in an email unprompted. The contact id is a random uuid only Resend and the address owner see, so it is the whole credential; anything that is not a uuid is refused without calling Resend. Rate-limited (newsletter-confirm: bucket, 10/min-window/IP).

Request Body

multipart/form-data

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

curl -X POST "https://example.com/api/newsletter/confirm" \  -F id="497f6eca-6276-4993-bfeb-53cbbbba6f08"
Empty

Sign up for Gaplessly product updates (public) POST

The marketing footer's newsletter sign-up, double opt-in with all state in Resend (lib/marketing/newsletter.ts): a new address becomes a Resend contact with `unsubscribed: true` and is mailed a confirmation link carrying the contact id; nothing is subscribed until `POST /api/newsletter/confirm`. A still-pending address gets the link again (Resend's own idempotency key limits that to once per 24h); a confirmed subscriber is sent nothing. Rate-limited (`newsletter:` bucket, 5/min-window/IP) and bot-checked (`guestBotCheck()`, registered in `bot-paths.ts`), since it mails whatever address it is given. Answers `{ok:true}` for new, pending and confirmed addresses alike, so it cannot be used to learn who is on the list. A form-encoded body (the footer form before hydration) gets a 303 to `/newsletter/confirm?status=sent` instead of JSON.

Update organization settings (admin, or manage_org_settings, or deposit_staff_editable) PATCH

A true partial update: a key that is not in the body is not in the UPDATE. This paragraph used to say the opposite, and the opposite used to be true; every field was derived with a fallback, so omitting `tagline` nulled it, omitting `notifyEmailEnabled` forced it `true`, and omitting `bookingTheme` reset it to `light`. That was fixed in the route (a `has()` gate, the same discipline the reservations, appointments and clients PATCH routes use) and the description was not moved with it. `scripts/check-hardening.ts` asserts the current behaviour column by column. **Three ways in (migration 0082), checked in order of breadth.** An admin may write anything below. A staff login granted `manage_org_settings` may write anything EXCEPT five admin-only keys that either change the org's public identity (`slug`, `status`) or grant a permission (`depositStaffEditable`, `unlinkedStaffFullAccess`, `staffClientVisibility`); a permission that can grant permissions is not delegable, it is the thing doing the delegating. Absent that, a staff login may still write the six deposit-policy fields alone, but only once this org's admin has turned on `depositStaffEditable` (migration 0075). `slug`, `status` and the three admin-only permission flags are written with service-role, because migration 0022 (and 0075 for the deposit flag) leaves those columns out of the tenant column grant; a staff JWT provably could flip them through raw PostgREST before that. Everything else goes through the RLS-scoped client. `business_type` is NOT settable. Flipping a live org's vertical strands whatever it already has; a conversion is a data migration, not a settings toggle.