SendBeam

Suppression list

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

View as Markdown

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;
  • 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 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.

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. Anyone on the suppression list arrives as unsubscribed automatically.
lightbulb
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.

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.

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

  • 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 (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.

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

Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.