Skip to content

Unsubscribes and suppression

Extrovert adds unsubscribe headers to eligible outbound mail and checks recipient suppression before a message enters review or is delivered. Do not add your own List-Unsubscribe or List-Unsubscribe-Post headers. Extrovert removes those custom values and writes the managed headers.

MessageHeaders
One To recipient with no cc or bccList-Unsubscribe with a recipient-bound token. List-Unsubscribe-Post is added only when one-click advertising is enabled.
Several recipients across to, cc, or bccList-Unsubscribe with a preferences link. A one-click token is not used because it cannot identify which recipient clicked.

The link resolves through the configured Extrovert API base URL. Its token is stateless and encrypted. No suppression row is written until the recipient acts.

RFC 8058 requires the unsubscribe headers to be covered by a valid DKIM signature. The current delivery path does not sign that header set, so Extrovert does not advertise one-click support by default. The HTTPS unsubscribe link remains available. Deployments that sign the required headers can enable the implemented POST path.

An opt-out applies to the recipient across the organization by default. It does not affect another tenant.

Two platform protections can broaden enforcement:

  • Mail from a shared sending domain can suppress the recipient across tenants that use that domain.
  • Opt-outs from three or more distinct organizations within 90 days can create a platform-wide backstop.

Only authenticated recipient actions count toward broader enforcement. Cross-tenant and platform rows are not exposed through an organization’s API.

If any recipient is suppressed, the whole send fails:

{
"type": "https://extrovert.dev/problems/recipient_suppressed",
"title": "Recipient Suppressed",
"status": 422,
"detail": "one or more recipients have opted out",
"code": "recipient_suppressed",
"request_id": "req_8Tz4kP",
"errors": [
{
"field": "recipient",
"code": "recipient_suppressed",
"detail": "alice@example.com"
}
]
}

Extrovert does not silently remove one address from a multi-recipient message. The errors array identifies the blocked address so the caller can decide whether to create a different message.

Suppression is checked at submission and again at delivery. A blocked message never enters the human review queue, and a new suppression can stop a message that is already waiting for approval. Suppression is checked before the intent requirement, so the response is 422 recipient_suppressed, not 422 intent_required.

The error does not reveal whether the matching row belongs to the organization, a shared domain, or the platform backstop.

The agent-plane suppression endpoints require read permission:

  • GET /v1/suppressions?recipient=<address> checks the organization’s visible suppression rows for an address.
  • GET /v1/suppressions lists visible rows and accepts recipient, scope, and include_revoked filters.
  • POST /v1/suppressions/{id}/revoke revokes an organization-owned row. A reason is required.

The scope query value and row field are stable wire names. Public instructions describe where a row applies.

Global and shared-domain rows can be changed only by a platform operator. The console provides the same organization-owned list, search, export, and reasoned revocation controls.

Extrovert checks the new, unquoted text of incoming replies conservatively:

  • A clear reply such as “stop,” “unsubscribe,” “remove me,” or “opt out” records an organization suppression.
  • A phrase inside a longer, qualified request records a review flag instead of making the choice automatically.
  • A phrase found only in quoted text does nothing.

Every accepted opt-out produces the same unsubscribe.received event, audit row, and configured webhook regardless of whether it came from a link, mailto:, preference page, or reply.