# Suppressions

Every workspace owns a suppression list: a set of email addresses Bird will not deliver to. Hard bounces, spam complaints, and unsubscribes land on it automatically, and you can add addresses yourself via the API. The list exists to protect your sender reputation: repeatedly mailing addresses that bounce or report spam is the fastest way to get your domain blocked by mailbox providers, so Bird stops those sends before they leave the platform.

Suppression is a recipient-level concept. A single send can have some recipients delivered and others suppressed; suppressed recipients are visibly rejected rather than silently dropped, so you can always see exactly which addresses were blocked and why.

You can browse and manage the list on the [**Suppressions**](https://bird.com/dashboard/w/email/suppressions) page (**Email → Suppressions**) in the dashboard, or use the [suppressions API](/docs/api/reference/list-suppressions).

![The Suppressions page in the Bird dashboard, listing suppressed addresses with their reason and origin](/images/docs/dashboard-email-suppressions.png)

## The four reasons and what they block

Each suppression record has a `reason` explaining why the address is on the list, and an `applies_to` policy controlling which [categories](/docs/guides/email/categories) it blocks:

| Reason        | `applies_to`        | Marketing category | Transactional category |
| ------------- | ------------------- | ------------------ | ---------------------- |
| `hard_bounce` | `all`               | Blocked            | Blocked                |
| `complaint`   | `non_transactional` | Blocked            | Allowed                |
| `unsubscribe` | `non_transactional` | Blocked            | Allowed                |
| `manual`      | `all`               | Blocked            | Blocked                |

The split follows from what each reason means:

- **`hard_bounce`**: the address does not exist. Sending is pointless in any category, so it blocks everything (`applies_to: all`).
- **`complaint`** and **`unsubscribe`**: preference signals. Someone who reported your newsletter as spam may still legitimately need a password reset or an order confirmation, so these block only non-transactional sends (`applies_to: non_transactional`). Sends in the `marketing` category are blocked; sends in the `transactional` category get through.
- **`manual`**: a deliberate decision by you or your team. Bird does not second-guess it: manual suppressions block every category, including transactional.

An address can hold one record per reason, so a hard bounce and an earlier unsubscribe coexist as separate records, and delivery stays blocked while any blocking record remains. `reason`, `origin`, and `applies_to` are open vocabularies that grow over time: treat an unknown `reason` or `origin` as informational, and an unknown `applies_to` as blocking at least non-transactional mail.

## How addresses get added automatically

Bird's delivery pipeline adds suppressions in response to recipient signals, so you never have to act on a bounce or complaint yourself:

| Trigger                                              | Resulting suppression                                                               |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Hard bounce (`email.bounced`)                        | `reason: hard_bounce`, `origin: bounce_event`, `applies_to: all`                    |
| Out-of-band hard bounce (`email.out_of_band_bounce`) | `reason: hard_bounce`, `origin: bounce_event`, `applies_to: all`                    |
| Spam complaint (`email.complained`)                  | `reason: complaint`, `origin: complaint_event`, `applies_to: non_transactional`     |
| In-body link unsubscribe (`email.unsubscribed`)      | `reason: unsubscribe`, `origin: unsubscribe_event`, `applies_to: non_transactional` |
| One-click unsubscribe (`email.list_unsubscribed`)    | `reason: unsubscribe`, `origin: unsubscribe_event`, `applies_to: non_transactional` |

Only a **hard**-class bounce suppresses. Soft, block, admin, and undetermined bounces leave the address sendable; the [classification table](/docs/guides/email/events#bounce-classification) shows which `bounce_class` values count as hard. There is no setting behind any of this: hard bounces, complaints, and unsubscribes always suppress.

What does **not** suppress matters as much:

- **Soft bounces and deferrals** (`email.deferred`, or `email.bounced` with `bounce_type: "soft"`): transient failures like a full mailbox. Bird retries; the address stays sendable.
- **Send-side rejections**: generation failures and policy rejections are problems with the send, not with the recipient's address. They produce `email.rejected` events but never a suppression.

Auto-suppression is idempotent and first-cause-stable: when an address is already suppressed for the same scope and reason, later matching events leave the original record unchanged, including its `source_email_id`, `source_recipient_id`, and `created_at`. Those two source fields link an automatic suppression back to the exact message and recipient that caused it, which answers the support question "why did this person stop getting our email"; they are null on manual additions.

Every addition to the list, automatic or manual, fires an `email_suppression.created` event to your [webhook endpoint](/docs/guides/webhooks). Its payload carries the `suppression_id`, the suppressed `email`, the `reason`, and the `workspace_id`, so your own system can mirror the list without polling:

```json
{
  "type": "email_suppression.created",
  "timestamp": "2026-07-23T14:52:03.192524705Z",
  "data": {
    "email": "user@example.com",
    "reason": "manual",
    "suppression_id": "sup_01ky7qckqrf06r38g49b9kxdbc",
    "workspace_id": "ws_01ky7m21hjffh9s8kq76gyb1xf"
  }
}
```

## Managing suppressions via the API

The API supports single-record add, list, lookup, and delete. Email addresses are normalized to lowercase before storage and lookup, and they never appear in URL paths: addresses are PII, and keeping them out of paths keeps them out of access logs. To find a record for an address, filter the list with `?email=` instead.

### Add an address

```bash
curl -X POST https://us1.platform.bird.com/v1/email/suppressions \
  -H "Authorization: Bearer $BIRD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@example.com" }'
```

Manual additions always get `reason: manual` and `applies_to: all`; they block every category. The call is idempotent: a new suppression returns `201 Created`, and if the address is already manually suppressed you get `200 OK` with the existing record instead of a conflict error. Either way the body is the suppression object:

```json
{
  "applies_to": "all",
  "created_at": "2026-07-23T14:52:03.192524705Z",
  "email": "user@example.com",
  "id": "sup_01ky7qckqrf06r38g49b9kxdbc",
  "origin": "api_key",
  "reason": "manual",
  "scope": {
    "id": "ws_01ky7m21hjffh9s8kq76gyb1xf",
    "type": "workspace"
  }
}
```

The `origin` field records how the suppression came to exist: `api_key` or `user` for manual additions (depending on whether the caller authenticated with an API key or a dashboard session), `bounce_event`, `complaint_event`, or `unsubscribe_event` for additions driven by delivery events, and `unsubscribe_link` when the recipient opted out through the Bird-hosted [unsubscribe page](/docs/guides/email/unsubscribe-links).

### List and look up

```bash
curl "https://us1.platform.bird.com/v1/email/suppressions?limit=25" \
  -H "Authorization: Bearer $BIRD_API_KEY"
```

The list is cursor-paginated and filterable by `reason`. To check whether a specific address is suppressed, pass it as the `email` query parameter:

```bash
curl "https://us1.platform.bird.com/v1/email/suppressions?email=user@example.com" \
  -H "Authorization: Bearer $BIRD_API_KEY"
```

An empty `data` array means the address is not suppressed. The same address can return multiple records when different reasons apply, for example a `hard_bounce` and an `unsubscribe`. The `email` filter matches by prefix: a complete address returns that address's records, and a partial value such as `alice` returns every suppressed address starting with it. Matching is case-insensitive.

### Remove an address

```bash
curl -X DELETE https://us1.platform.bird.com/v1/email/suppressions/sup_01ky7qckqrf06r38g49b9kxdbc \
  -H "Authorization: Bearer $BIRD_API_KEY"
```

Returns `204 No Content`. This is a **hard delete**: the record is removed entirely, Bird retains nothing, and the address is immediately sendable again. An address suppressed for several reasons has one record per reason, so delete each blocking record to re-enable delivery. To delete by address, look up the ID with `?email=` first, then delete by ID. Be deliberate about removing `hard_bounce` records: if the address still doesn't exist, the next send bounces and re-suppresses it.

## What happens when you send to a suppressed address

Suppressed recipients are **rejected, not silently dropped**. When a send includes a suppressed address:

- The recipient still gets a `recipient_id` and appears in the message's recipient list with status `rejected`.
- An `email.rejected` event is recorded for that recipient with `rejection_reason: "recipient_suppressed"`, visible in the [events API and your webhooks](/docs/guides/email/events).
- The message itself is still accepted (`202`) and delivery proceeds for the remaining recipients.

This holds even when **every** recipient of a send is suppressed: the send is still accepted (`202`) and a message ID is created, and each suppressed recipient surfaces as `rejected` with `rejection_reason: "recipient_suppressed"`. Suppression is evaluated during processing, not at accept time, so an all-suppressed send never returns an up-front `422`.

This mirrors how other providers expose suppression (SendGrid's `dropped`, Postmark's suppressed bounces), so you can audit exactly which addresses were blocked instead of inferring it from missing deliveries.

## Testing with the sandbox

The [testing sandbox](/docs/guides/email/testing-sandbox) gives you a deterministic way to exercise suppression handling: sending to `suppressed@messagebird.dev` short-circuits as if the address were on your suppression list, so the recipient is rejected with `rejection_reason: "recipient_suppressed"` and never reaches delivery. Sandbox bounce and complaint addresses (`bounce@messagebird.dev`, `complaint@messagebird.dev`) simulate their outcomes through the real event pipeline but do **not** write to your suppression list, so the same test addresses stay reusable across runs.

## Next steps

- [Categories](/docs/guides/email/categories): `transactional` vs `marketing` and how the category interacts with suppression policy
- [Unsubscribe links](/docs/guides/email/unsubscribe-links): how opt-outs reach the suppression list with reason `unsubscribe`
- [Events and webhooks](/docs/guides/email/events): the `email.rejected` payload and the lifecycle events that drive auto-suppression
- [Testing sandbox](/docs/guides/email/testing-sandbox): magic addresses for simulating every delivery outcome
- [API reference: Suppressions](/docs/api/reference/list-suppressions): full request and response schemas