# Suppression list

Addresses that are never emailed, and how to bring your old platform's unsubscribes, bounces and complaints across before you send.

Suppressed addresses are never emailed, whatever list or segment they are in. The suppression
list is where SendBeam remembers everyone who unsubscribed, bounced, complained or was deleted,
so that a later import or API call cannot quietly bring them back as a subscriber.

## What the suppression list is

Every workspace has one suppression list. An address lands on it when:

- a subscriber clicks **unsubscribe** in one of your emails (`unsubscribed`);
- you set a contact to Unsubscribed with **Also block this address** ticked — on the edit page, in the bulk **Unsubscribe** action, or with `suppress: true` on the API (`unsubscribed`). Without that, an owner-set unsubscribe is a status only and does not touch this list — see [marked versus blocked](https://sendbeam.io/docs/contacts/editing#marked-vs-blocked);
- a message to them hard-bounces (`bounced`);
- they report a message as spam (`complained`);
- you delete the contact (`deleted`), so the address does not come back on the next import;
- you import it, from the dashboard or the API, as described on this page.

The list is enforced wherever an address could otherwise become a subscriber again: a
[CSV contact import](https://sendbeam.io/docs/contacts/importing) brings a suppressed address in as
`unsubscribed` whatever the file says, `POST /api/v1/contacts` refuses it
with `409`, and editing a contact's email to a suppressed address or setting a
suppressed contact's status back to `subscribed` is refused the same way. Campaigns
and automations only ever go to contacts whose status is `subscribed`. The one way
off the list is a fresh opt-in from the person themselves, and only for an unsubscribe — see
[When someone subscribes again](#resubscribe).

Reasons have a strength order — `complained` beats `bounced`, which beats
`unsubscribed`, which beats `deleted`. A stronger reason replaces a weaker
one; a weaker one never downgrades a stronger one. Importing the same file twice is therefore
harmless.

## Import it before your first send

When you move to SendBeam from another platform, load its unsubscribes, bounces and complaints
*before* you import contacts and long before you send. Your previous platform kept those
people out of your sends for you; a fresh export of "all contacts" often includes them with no
warning, and one campaign to a few hundred old complainers is enough to damage a sending
domain's reputation.

1. Export the unsubscribed, cleaned/bounced and complained (junk/abuse) lists from your old platform. Most tools offer these as separate CSV downloads or as a status column in the main export.
2. Import them here — **Contacts → Suppressions** in the dashboard, or `POST /api/v1/suppressions`.
3. Then [import your contacts](https://sendbeam.io/docs/contacts/importing). Anyone on the suppression list arrives as `unsubscribed` automatically.

> Already imported contacts? Importing suppressions afterwards still works: any matching contact
> who is still `subscribed` is switched to the imported status at the same time. No
> contact is ever created by a suppression import.
> 

## Importing from the dashboard

1. Open **Contacts**, then **Suppressions** in the **⋯** menu at the top of the page. The summary shows how many addresses are on the list, by reason.
2. Drop your CSV on the upload area (up to 5 MB per file — split larger exports).
3. Choose the reason to apply to rows that do not carry one. It defaults to **Unsubscribed**; pick **Bounced** or **Complained** when you are uploading a dedicated bounce or complaint export.
4. Click **Import suppressions**. The result tells you how many addresses were added, upgraded to a stronger reason, already on the list, or skipped as invalid, and how many existing contacts were switched from subscribed.

## CSV format

A UTF-8 CSV with a header row. The `email` column is required (`email_address`
is also accepted); `reason` (or `status`) is optional. Headers are
case-insensitive. A file that is just one address per line, with no header, is accepted too.

```
email,reason
alice@example.com,unsubscribed
bob@example.com,cleaned
carol@example.com,spam
dave@example.com,
```

Reason cells are matched case-insensitively and the words other platforms use are understood:

- `unsubscribed` — also `unsubscribe`, `cancelled`, `canceled`, `opted out`, `optout`.
- `bounced` — also `bounce`, `cleaned`, `hard bounce`, `invalid`.
- `complained` — also `complaint`, `junk`, `spam`, `abuse`.

A blank reason takes the default you chose (the `reason` form field for the API;
`unsubscribed` when none is given). A row whose reason is anything else, or whose
address is missing, malformed or over 254 characters, is skipped and counted as
`invalid`. `deleted` cannot be imported; it is reserved for contacts you
delete in SendBeam. Repeated addresses in one file are folded together, keeping the stronger
reason.

## Importing with the API

`POST /api/v1/suppressions` takes either JSON or the same CSV as a multipart upload.
It needs an API key with `contacts:write` (available on every plan; writes are metered per hour, like every
other write). Reasons and aliases are the same as for the CSV.

**JSON** — up to 5,000 entries per request; send several requests for more. `reason` on an entry is optional, and a top-level `reason` sets the default for entries without one:

```
curl -X POST https://sendbeam.io/api/v1/suppressions \
  -H "x-api-key: sb_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "unsubscribed",
    "entries": [
      { "email": "alice@example.com" },
      { "email": "bob@example.com", "reason": "bounced" },
      { "email": "carol@example.com", "reason": "complained" }
    ]
  }'
```

**CSV** — the file goes in a field named `file` (5 MB max); an optional `reason` field is the default for blank cells:

```
curl -X POST https://sendbeam.io/api/v1/suppressions \
  -H "x-api-key: sb_live_..." \
  -F "file=@bounces.csv" \
  -F "reason=bounced"
```

Both return the same summary:

```
{
  "received": 3,
  "added": 2,
  "upgraded": 1,
  "unchanged": 0,
  "invalid": 0,
  "contacts_updated": 1,
  "by_reason": { "unsubscribed": 1, "bounced": 1, "complained": 1 }
}
```

- `received` — rows or entries in the request. Always equals `added + upgraded + unchanged + invalid`.
- `added` — addresses that were not on the list before.
- `upgraded` — addresses already on the list whose reason became stronger.
- `unchanged` — already on the list with the same or a stronger reason (repeats within the request count here too).
- `invalid` — rows skipped for a bad address or an unknown reason.
- `contacts_updated` — existing contacts switched from `subscribed` to the imported status (`unsubscribed_at` is set to now).
- `by_reason` — the valid, de-duplicated addresses split by the reason that was applied.

Full request and response schemas are in the [API reference](https://sendbeam.io/docs/api#post-api-v1-suppressions).

## Checking an address and counts

`GET /api/v1/suppressions` (key with `contacts:read`) returns the size of
the list by reason — the same figures the dashboard shows:

```
curl https://sendbeam.io/api/v1/suppressions -H "x-api-key: sb_live_..."

{ "count": 1204, "by_reason": { "unsubscribed": 1130, "bounced": 61, "complained": 9, "deleted": 4 } }
```

Add `?email=` to ask about one address:

```
curl "https://sendbeam.io/api/v1/suppressions?email=bob@example.com" -H "x-api-key: sb_live_..."

{ "email": "bob@example.com", "suppressed": true, "reason": "bounced", "created_at": "2026-09-03T09:12:41.000Z" }
```

An address that is not on the list returns `{ "email": "...", "suppressed": false }`.
The dashboard's **Check an address** box calls the same endpoint.

## Browsing and exporting the list

The **Browse the list** table on the Suppressions page shows every row, newest
first: a masked address (`j***@example.com`), its domain, the reason, where the
suppression came from, and when it was added. Filter by reason or by domain, and click
**Export CSV** to download the rows shown. Rows recorded before 14 September 2026
show "—" for the address and domain: only their hash was ever kept, and SendBeam does not
guess at the rest. Where a contact record still carries the address (an unsubscribed or
bounced contact you have not deleted, say) the row links to it, and the export fills in the
full address for that row.

The **source** column says how the address got there: *Unsubscribe link*
(the person clicked it or used the preference page), *Delivery report* (a bounce or spam
report from the mailbox provider), *Import*, *Blocked in app* (you set
Unsubscribed with the block box ticked), *Contact deleted*, or *Erased on request*.

From a script: `GET /api/v1/suppressions/list` pages the rows (`page`,
`limit`, `reason`, `domain`) and
`GET /api/v1/suppressions/export` returns the CSV — columns `email` (only
where a contact record still exists), `email_masked`, `domain`,
`reason`, `source`, `added_at`, `contact_id`. The
list needs `contacts:read`; the export needs `contacts:export`, the same
permission as the [contacts export](https://sendbeam.io/docs/contacts/exporting).

## Lifting a block

A suppression is a record of what a person or a mailbox told you, so most rows are never
removed by anyone in the workspace. The one exception is the block a **deleted contact** leaves behind: deleting a test contact, a duplicate or a colleague's old
address should not mean the address is lost for good. A workspace admin can click
**Lift block** on such a row (or send `POST /api/v1/suppressions/lift`
with the row's `email` or `email_hash`), after which an import, the API
or a signup form can add the address again. The deleted contact itself is not restored.

- `bounced` and `complained` rows are never lifted: the address is dead, or the person reported you.
- An `unsubscribed` row is the person's decision: it is lifted only by their own new opt-in, or by you asserting their new consent through [re-subscribe](https://sendbeam.io/docs/contacts/editing#re-subscribing).
- An address **erased** at the person's request (the *Erase* action on a contact) is never lifted from the app; only the person can return, through a form.

If you do not want the address blocked at all, [archive](https://sendbeam.io/docs/contacts/editing#archive-delete-erase)
the contact instead of deleting it.

## How addresses are stored

Every row stores a one-way hash of the lower-cased address, never the address itself. That is
why the list can outlive the contact record — it keeps working after a deletion without retaining
the person's data, which is what our [data processing agreement](https://sendbeam.io/legal/dpa) promises.
Rows written since 14 September 2026 also carry a masked display form (the first character of
the local part, then `***`, then the domain; a one- or two-character local part is
masked entirely, and a `+tag` is dropped) and the bare domain, so that you can review
the list without SendBeam holding the address. Older rows are never back-filled. An address
erased at a person's request keeps the hash alone whatever else was known.

## When someone subscribes again

Being on the list does not stop a person from choosing to come back — but only an
`unsubscribed` entry is theirs to undo. A bounce says the address is dead, a complaint
says they reported you, and an erasure was their own request: none of those is cleared by
a new signup. The block a plain deletion leaves can also be lifted by a workspace admin — see
[Lifting a block](#lift).

- **Through one of your signup forms**: the submission creates or updates the contact
  as `subscribed` (`source: form`) and, if the address was
  `unsubscribed`, removes it from the suppression list at the moment the opt-in is
  complete. For a form whose list does not use double opt-in (or no list at all) that is the
  submission itself. If the list uses [double opt-in](https://sendbeam.io/docs/lists/double-optin) (or
  the plan requires it), the entry stays until they click the confirmation link — confirming
  removes it, marks the list membership confirmed and fires *list joined* automations.
  An address that `bounced` or `complained` is not resurrected: the visitor
  still sees the form's thank-you message (a public form must not reveal whether an address is
  known), the submission is recorded under the form, but the contact is left as it is, no list is
  joined and no confirmation email is sent. A `deleted` address is created afresh as
  `subscribed` but its entry stays on the list.
- **Through the dashboard or API**: a CSV contact import brings a suppressed address
  in as `unsubscribed`, because a spreadsheet is not consent. `POST /api/v1/contacts`,
  changing a contact's email to a suppressed address, and setting a suppressed contact's status
  back to `subscribed` all return `409`. If the person has given you new
  consent after *unsubscribing* — they asked you in person, say — send
  `{ "resubscribe": true }` with `POST /api/v1/contacts` or
  `PATCH /api/v1/contacts/{id}`: the contact becomes `subscribed`
  with `subscribed_at` set to now and `source` unchanged, and the
  `unsubscribed` entry is removed once the write succeeds. `resubscribe`
  has no effect on a bounced, complained or deleted address: those remain `409`.

Once an entry has been cleared this way, `GET /api/v1/suppressions?email=` reports the
address as not suppressed and a later contact or suppression import treats it like any other
address — unless the import itself carries an opt-out status for it, which is applied as usual.

> Migrating in stages? Import your old platform's suppressions *before* your forms go live
> on SendBeam. Anyone who re-subscribes through a form after the import is taken off the list
> when they do, so the order only matters for people who re-subscribed on the old platform in
> between.
>

---
Source: https://sendbeam.io/docs/contacts/suppressions
