Skip to content

Unsubscribes

Suppression is a first-class list, not a flag on a contact.

Why it is keyed by address

Opt-outs are stored per (organization, email address) rather than per contact, because:

  • POST /v1/send takes free-form recipients that may have no contact record — in practice most suppressed addresses never had one.
  • The record must outlive contact deletion. Deleting someone must not resubscribe them.

Where it is enforced

PathBehaviour
POST /v1/sendSuppressed recipients are dropped before the message row and before quota is spent. If all recipients were suppressed, the send fails 422 and costs nothing. A partially suppressed send still reaches the survivors.
Automation send_emailThe step is recorded as skipped.
BroadcastsExcluded during audience resolution.
Platform mail (password reset, MFA notices)Exempt by design. A marketing opt-out must never block a password reset.

The hosted page

Every organization gets an opt-out page at unsubscribe.<DOMAIN>. Its copy is editable from Settings → General; every field falls back to a default, so an organization that never touches it still has a working page.

Links are stateless: the ?t= token is a signature over the organization, the address and an issue time. Nothing is stored per link, so a token in already-delivered mail keeps working and there is no table that grows with your send volume.

GET renders the page and POST records the opt-out — split so that a link scanner or a prefetching mail client cannot unsubscribe someone who merely received the email.

Managing the list

bash
curl http://api.localhost/v1/unsubscribes -H "X-API-Key: $KEY"
curl -X DELETE http://api.localhost/v1/unsubscribes/$ID -H "X-API-Key: $KEY"

Removing an entry emits contact.resubscribed. Both write paths emit only on a real state change, so a repeated call is a silent no-op rather than a storm of duplicate webhooks.

Released under the Apache 2.0 License.