# 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.

## Automatic unsubscribe headers

| Message | Headers |
|---|---|
| One `To` recipient with no `cc` or `bcc` | `List-Unsubscribe` with a recipient-bound token. `List-Unsubscribe-Post` is added only when one-click advertising is enabled. |
| Several recipients across `to`, `cc`, or `bcc` | `List-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.

### One-click advertising is off by default

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.

## Where an opt-out applies

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.

## The `recipient_suppressed` error

If any recipient is suppressed, the whole send fails:

```json
{
  "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.
**Replies can be allowed:** A new send and a forward are blocked. A reply can proceed when it answers a message from that
  recipient that arrived after the suppression was recorded. This lets a recipient restart the
  conversation.

## Check, list, and revoke

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.
**Revocation is audited:** Revocation stops future enforcement but does not erase the original opt-out or audit record.
  Repeated unsubscribe and revoke cycles can trigger an abuse review.

## Opt-outs in replies

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.

## Next

- [Webhooks and HMAC](https://docs.extrovert.dev/concepts/webhooks/)
- [API errors](https://docs.extrovert.dev/api/errors/)
- [Deliverability and limits](https://docs.extrovert.dev/concepts/deliverability-and-limits/)