Unsubscribe
| Method | Path | Auth |
|---|---|---|
GET | /api/v1/unsubscribe?c=<contactId>&t=<token> | Signed token — no API key |
POST | /api/v1/unsubscribe?c=<contactId>&t=<token> | Signed token — no API key |
This is the endpoint behind the unsubscribe link in every marketing email. You do
not normally call it: Mailroom injects the fully-formed URL as unsubscribeUrl when
it renders a marketing send, and sets List-Unsubscribe /
List-Unsubscribe-Post: List-Unsubscribe=One-Click headers so mail clients can offer
their own unsubscribe button.
Authentication
No API key. The t token is an HMAC over the contact id, signed with the project's
own unsubscribe secret, so a token minted for one project never verifies under
another and one cannot be forged from a contact id alone.
An invalid, mismatched, or missing token is rejected — there is no way to enumerate or opt out arbitrary contacts through this route.
GET — the human path
Returns a small HTML page, not JSON:
- 200 — a confirmation page; the contact's status is now
unsubscribed. - 400 — an "invalid link" page when the token does not verify.
POST — the one-click path
For List-Unsubscribe-Post. Bare status, no body:
- 200 — unsubscribed.
- 400 — token did not verify.
Mail clients call this without user interaction, so it must stay idempotent: unsubscribing an already-unsubscribed contact is still a 200.
Side effects
The contact's status flips to unsubscribed and a contact.unsubscribed
webhook fires — subscribe to it to mirror the opt-out into your own
store. Only the first transition dispatches the event; repeat hits are no-ops.
Programmatic opt-out
To unsubscribe someone from your own code, use the authenticated endpoint instead:
POST /api/v1/contacts/:id/unsubscribe.
Preserving links during a migration
Because the URL carries ?c=<contactId>, links already in inboxes keep working only
if the contact id stays the same. Pass your existing uuid as id when you upsert —
see contacts.