Context
Two mechanisms that look similar and are not, and conflating them is a real bug. Consent is what the person chose, and it lives incore. Suppression is what the system or
the provider imposed: a hard bounce, a spam complaint, an unsubscribe click. A person can consent
and still be suppressed — the address simply does not work — and a suppressed address must stay
suppressed even for transactional messages, because a bounce does not become deliverable because
the message is important.
The preference center is the third layer: within what they consented to, a member chooses the
granularity. Making it one click from any marketing message is not a courtesy; a hard-to-find
unsubscribe is what generates spam complaints, and complaints damage the shared sending reputation
of every tenant.
Scope (normative)
messaging.suppressions: global per tenant, per channel identifier.- Automatic suppression on hard bounce, complaint and unsubscribe.
messaging.preference_settings: per member, per channel, per category.- One-click unsubscribe from any marketing message, including the list-unsubscribe header.
- Quiet hours and frequency caps configuration at tenant and campaign level.
- Suppression survival through erasure as a hashed identifier.
Non-scope (normative)
- Consent, which belongs to
coreand is read here. - Manual suppression import from a previous provider — a later import feature, though the table supports it.
- Re-engagement of suppressed addresses. A suppression is only lifted by the member acting.
Behaviour (normative)
- A suppression is keyed by (tenant, channel, identifier) — the email address or push token, not the contact. If the same address belongs to two contacts, both are suppressed, because the address is what bounced.
- Every send checks suppressions, no exceptions, including transactional (FS-MSG-0001 rule 3).
- Hard bounces and complaints suppress automatically, in the same transaction as the status (FS-MSG-0003). Soft bounces do not: a full mailbox is temporary.
- Unsubscribe is one click, no login, from a signed link that identifies the member without authenticating them. Requiring a login to unsubscribe generates complaints.
- The
List-UnsubscribeandList-Unsubscribe-Postheaders are set on every marketing message, so a mail client’s own button works. - A suppression can be lifted only by the member re-subscribing, never by the tenant. A tenant un-suppressing an address that complained is exactly the behaviour that gets a domain blocked.
- The preference center is per channel and per category. Transactional appears as informational and cannot be switched off (DEC-E3).
- Quiet hours default at tenant level with campaign override; frequency caps apply to marketing only (DEC-E5).
- Erasure deletes the profile but the suppression survives as a hashed identifier with no profile attached (DEC-J3).
- FORBIDDEN: a tenant-facing endpoint that deletes a suppression · a marketing message without a working unsubscribe · counting transactional against a frequency cap.
Data (normative)
The identifier is stored hashed so a suppression list is not a usable contact list.
API (normative)
The unsubscribe route lives at the root, outside
/v1, because it is a link in an email that must
stay short, stable and human-readable. Documented exception to DEC-D1, like the OAuth routes.
Events (normative)
None beyond the status events FS-MSG-0003 emits. A suppression is a consequence, not an occurrence.Acceptance criteria (normative)
- A suppressed address receives nothing, including transactional and including a different contact sharing that address.
- A hard bounce suppresses in the same transaction; a soft bounce does not.
- Unsubscribe works from the signed link without a login, in one request.
List-Unsubscribeheaders are present on every marketing message and function in a real client.- No tenant-facing endpoint removes a suppression.
- The preference center cannot switch off transactional.
- Frequency caps count marketing only.
- After erasure the suppression still blocks that address, with no profile attached.
- Negative: the suppression list cannot be exported as usable contact data — identifiers are hashed.