# Suppressions API

> Add, list, remove, and check suppressed email addresses with the Broadcast API. Manage a channel's suppression list and the installation-wide global list.

Source: https://sendbroadcast.net/docs/api-suppressions

A suppressed address is one Broadcast will not email by default, though what gets blocked depends on where the suppression came from. Bounces and complaints add addresses automatically, and you can manage the lists yourself through the API.

There are two lists, and they are separate resources:

- **Channel suppressions** use `/api/v1/suppressions` and block the address for
  one channel. The same person can still be emailed from your other channels.
- **Global suppressions** use `/api/v1/global_suppressions` and block the address
  across every channel you have access to.

An address may appear on both. If either list says to block it, the email is not sent.

## Suppression Sources and Blocking Behavior

Each suppression has a source that indicates how it was created. The source affects what the suppression blocks:

**Added by you (manual, import, api):** Blocks all email, including transactional emails.

**Reported by provider (provider_bounce, provider_complaint, provider_suppression):** Blocks marketing email only (broadcasts and sequences). Transactional emails still go through. Hard bounces can optionally be configured per channel to also block transactional email, though the API does not report this setting.

When multiple sources exist for the same address, the strongest one wins: customer-added suppressions take precedence, then provider reports. Adding an address via API that was already suppressed as a provider report upgrades its source to `api`.

## Which one do I want?

If an address bounced or complained in one channel and you never want to email
that person again from anywhere, use the **global** list. That is the usual
answer when you are importing bad addresses from another platform, where the
bounce history did not come across with the contacts.

Use the **channel** list when the block is specific to one brand or audience:
someone who unsubscribed from one newsletter but is a legitimate contact for
another.

## Required Permissions

All endpoints require an API token with suppression permissions:

- **Read**: `GET` endpoints, including the check endpoint
- **Write**: `POST` and `DELETE` endpoints

Warning

The **global** endpoints require an **admin API token**. A channel-scoped token can read and write only its own channel’s list. This is deliberate: the global list affects every channel on the installation, so a channel token that could write to it would reach far beyond the channel it was issued for.

Admin API tokens must send `broadcast_channel_id` on the channel endpoints, as
they do everywhere else in the API. They must **not** send it on the global
endpoints, because a global suppression does not belong to a channel.

Creating either kind of token, and the request rate limits that apply to all API
calls, are covered in [API Authentication](https://sendbroadcast.net/docs/api-authentication).

## The Suppression Object

| Field | Description |
| --- | --- |
| `id` | Unique identifier of the suppression |
| `email` | The suppressed address, always lowercased and trimmed |
| `scope` | `channel` or `global` |
| `broadcast_channel_id` | The channel it applies to, or `null` for a global suppression |
| `source` | How the suppression was created: `manual`, `import`, `api`, `provider_bounce`, `provider_complaint`, or `provider_suppression` |
| `reason` | For provider-reported suppressions, the provider's reason (for example the bounce description, or "AWS SES suppression list (BOUNCE)"). `null` for suppressions added manually |
| `created_at` | When the address was suppressed |

Addresses are normalised before they are stored and before they are compared, so
` Bob@Example.COM ` and `bob@example.com` are the same address throughout.

Suppressing an address **does not change the subscriber record**. The subscriber
stays active and stays in your counts; the address is simply skipped at send
time. Suppression is separate from unsubscribing. An unsubscribe is the
subscriber's own decision and is recorded against them, while a suppression is
yours.

## Check an Address

```
GET /api/v1/suppressions/check
```

Answers the question most integrations actually need: **will this address
receive mail?** It checks the global list first, then this channel's list, and
tells you which one matched.

Use this rather than fetching both lists and comparing them yourself. An address
blocked globally will not receive mail even though it appears nowhere in your
channel's list.

### Query Parameters

- `email` (required): the address to check

### Request

```bash
curl -X GET \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  "http://your-domain.com/api/v1/suppressions/check?email=bob@example.com"
```

### Response

```json
{
  "email": "bob@example.com",
  "suppressed": true,
  "scope": "global"
}
```

`scope` is `global` or `channel` when `suppressed` is `true`, and `null` when it
is `false`:

```json
{
  "email": "alice@example.com",
  "suppressed": false,
  "scope": null
}
```

This endpoint needs only **read** permission on a channel token. It reports one
address; it does not let a channel token browse the global list.

Check is channel-scoped, so an admin API token must name the channel it is asking
about, the same `broadcast_channel_id` the other channel endpoints take:

```bash
curl -X GET \
  -H "Authorization: Bearer YOUR_ADMIN_API_TOKEN" \
  "http://your-domain.com/api/v1/suppressions/check?email=bob@example.com&broadcast_channel_id=1"
```

## List Channel Suppressions

```
GET /api/v1/suppressions
```

Returns a paginated list of addresses suppressed for this channel. Global
suppressions are not included. Use the global endpoint for those, or the check
endpoint for a single address.

### Query Parameters

- `page`: Page number (default: 1). Each page holds up to 250 suppressions
- `email`: Filter to addresses containing this text

### Request

```bash
curl -X GET \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  http://your-domain.com/api/v1/suppressions
```

### Response

```json
{
  "suppressions": [
    {
      "id": 42,
      "email": "bob@example.com",
      "scope": "channel",
      "broadcast_channel_id": 1,
      "source": "provider_bounce",
      "reason": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
      "created_at": "2026-07-30T12:34:56Z"
    }
  ],
  "pagination": {
    "total": 1,
    "count": 1,
    "from": 1,
    "to": 1,
    "current": 1,
    "total_pages": 1
  }
}
```

## Add a Channel Suppression

```
POST /api/v1/suppressions
```

Adds one address to this channel's list.

### Parameters

- `email` (required): the address to suppress

### Request

```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "bob@example.com" }' \
  http://your-domain.com/api/v1/suppressions
```

### Response

```json
{
  "id": 42,
  "email": "bob@example.com",
  "scope": "channel",
  "broadcast_channel_id": 1,
  "source": "api",
  "reason": null,
  "created_at": "2026-07-30T12:34:56Z"
}
```

Suppressions added via API block all email, including transactional emails.

Adding an address that is already suppressed succeeds and returns the existing
record. A newly created suppression answers `201`; one that was already there answers `200`, so you can still tell the two apart if it matters. It is not an error either way, so you do not need to check before adding.

If an address is already suppressed as a provider report (e.g. hard bounce), adding it via API upgrades it to `source: "api"`, and the response returns the updated record with `200`.

## Bulk Add Channel Suppressions

```
POST /api/v1/suppressions/bulk
```

Adds up to **10,000** addresses in one request, all to this channel's list.

If you are importing bounce and complaint history from another platform, you
almost certainly want the **global** list instead. See
[Importing from another platform](#importing-from-another-platform) below.

### Parameters

- `emails` (required): an array of addresses

### Request

```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": [
      "bob@example.com",
      "carol@example.com",
      "not-an-email"
    ]
  }' \
  http://your-domain.com/api/v1/suppressions/bulk
```

### Response

```json
{
  "added": 2,
  "already_suppressed": 0,
  "invalid": ["not-an-email"]
}
```

Every address is accounted for:

- `added` counts the addresses newly suppressed
- `already_suppressed` counts those already on the list before this request
- `invalid` lists the entries that were not address-shaped, and were skipped

One malformed address does not fail the request. The rest are still added, and
the bad one comes back in `invalid` so you can correct your source data.

### What counts as invalid

A suppression list is a blocklist, so this endpoint checks shape rather than
strict RFC conformance. An address needs a single `@` with something either
side of it, and nothing else is required.

That means international addresses containing accented or non-Latin characters
are accepted here, as are addresses that your email provider might refuse at
send time. Recording an address you cannot currently mail costs nothing, while
refusing to record one means you keep mailing somebody who asked you to stop.
Note that this is more permissive than the subscriber endpoints, which do
require a conventional address.

Addresses come back in `invalid` when they could never match anything: a whole
CSV row rather than a single address, a line containing spaces, or text that
could not be decoded as UTF-8. Send your addresses as UTF-8. A list exported as
Latin-1 or Windows-1252 will have every accented address reported as invalid.

Duplicates inside a single request count once. Sending the same batch twice adds
nothing the second time, so a retry after a network timeout is safe.

Warning

More than 10,000 addresses in one request is rejected with `422`. Split larger imports into batches.

## Remove a Channel Suppression

```
DELETE /api/v1/suppressions
```

Removes an address from **this channel's** list. A global suppression for the
same address is left in place, so the address may still be blocked. Check with
the check endpoint if you expect mail to start flowing again.

### Query Parameters

- `email` (required): the address to remove

### Request

```bash
curl -X DELETE \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  "http://your-domain.com/api/v1/suppressions?email=bob@example.com"
```

### Response

```json
{
  "email": "bob@example.com",
  "removed": true
}
```

Removing an address that was not suppressed returns `"removed": false` with a
`200`. It is not an error.

## Bulk Remove Channel Suppressions

```
DELETE /api/v1/suppressions/bulk
```

Removes up to **10,000** addresses from this channel's list in one request. The
counterpart to bulk add. If an import used the wrong file, this is how you undo
it without one request per address.

### Parameters

- `emails` (required): an array of addresses

### Request

```bash
curl -X DELETE \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "emails": ["bob@example.com", "carol@example.com"] }' \
  http://your-domain.com/api/v1/suppressions/bulk
```

### Response

```json
{
  "removed": 2,
  "not_found": 0
}
```

Addresses that were not suppressed are counted in `not_found` rather than
failing the request, so re-running the same removal is safe.

## Global Suppressions

The global endpoints mirror the channel ones exactly and take the same
parameters. They require an **admin API token**, and must not be given a
`broadcast_channel_id`.

```
GET    /api/v1/global_suppressions            list, paginated
POST   /api/v1/global_suppressions            add one
POST   /api/v1/global_suppressions/bulk       add up to 10,000
DELETE /api/v1/global_suppressions?email=     remove one
DELETE /api/v1/global_suppressions/bulk       remove up to 10,000
```

### Add One

```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ADMIN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "bob@example.com" }' \
  http://your-domain.com/api/v1/global_suppressions
```

```json
{
  "id": 77,
  "email": "bob@example.com",
  "scope": "global",
  "broadcast_channel_id": null,
  "source": "api",
  "reason": null,
  "created_at": "2026-07-30T12:34:56Z"
}
```

### Add in Bulk

```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ADMIN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": [
      "bob@example.com",
      "carol@example.com",
      "not-an-email"
    ]
  }' \
  http://your-domain.com/api/v1/global_suppressions/bulk
```

```json
{
  "added": 2,
  "already_suppressed": 0,
  "invalid": ["not-an-email"]
}
```

The same rules as the channel bulk endpoint: one malformed address does not fail
the request, duplicates inside a batch count once, and re-sending a batch adds
nothing the second time.

Warning

`DELETE /api/v1/global_suppressions` removes the address from the global list only. Channel suppressions for the same address are left alone, and will keep blocking mail in those channels.

## Importing from Another Platform

Moving from another email platform is the case these endpoints were built for.
Contacts usually import cleanly while their bounce and complaint history does
not, so addresses that platform had already given up on arrive in Broadcast
looking perfectly healthy, and get mailed again.

Export the bounced and complained addresses from the old platform, reduce them to
a bare list of addresses, and load them into the **global** list before your first
send:

```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ADMIN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "emails": ["bob@example.com", "carol@example.com"] }' \
  http://your-domain.com/api/v1/global_suppressions/bulk
```

Global rather than per-channel, because a hard bounce is bad everywhere: the
address is dead regardless of which of your channels reaches it next.

**More than 10,000 addresses.** Split the list into batches of 10,000 or fewer
and send them one after another. Every batch reports its own counts, and because
re-sending a batch adds nothing you can safely retry one that timed out without
tracking which addresses it contained.

Note

API requests are rate limited. Each response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers. Read `X-RateLimit-Remaining` between batches and pause until the reset time rather than retrying blind. See [API Authentication](https://sendbroadcast.net/docs/api-authentication) for the current limits.

If the addresses you want to block are already on one channel's list, you do not
need the API at all. See
[Copying a channel's list to the global list](#copying-a-channels-list-to-the-global-list)
at the end of this page.

## Errors

| Status | Meaning |
| --- | --- |
| `400` | An admin API token called a channel endpoint without `broadcast_channel_id` |
| `401` | Missing or unrecognised token, the token lacks suppression permission, or a channel token called a global endpoint |
| `422` | `email` or `emails` missing, a single add whose `email` is not an address (`{"error": "email is invalid"}`), or a bulk request over 10,000 addresses |
| `404` | An admin API token sent a `broadcast_channel_id` that does not exist |
| `429` | Rate limit exceeded. Check `X-RateLimit-Reset` and retry after it |

## Copying a Channel's List to the Global List

To promote an entire channel's suppression list in one go, use
[Channel Maintenance](https://sendbroadcast.net/docs/channel-maintenance) in the app rather than the API. It
previews what would be copied, runs in the background for large lists, and keeps
the channel's own list intact.
