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: trueon 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.
- 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.
- Import them here — Contacts → Suppressions in the dashboard, or
POST /api/v1/suppressions. - Then import your contacts. Anyone on the suppression list arrives as
unsubscribedautomatically.
subscribed is switched to the imported status at the same time. No
contact is ever created by a suppression import.
Importing from the dashboard
- 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.
- Drop your CSV on the upload area (up to 5 MB per file — split larger exports).
- 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.
- 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— alsounsubscribe,cancelled,canceled,opted out,optout.bounced— alsobounce,cleaned,hard bounce,invalid.complained— alsocomplaint,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 equalsadded + 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 fromsubscribedto the imported status (unsubscribed_atis 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.
bouncedandcomplainedrows are never lifted: the address is dead, or the person reported you.- An
unsubscribedrow 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 wasunsubscribed, 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 thatbouncedorcomplainedis 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. Adeletedaddress is created afresh assubscribedbut 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 tosubscribedall return409. If the person has given you new consent after unsubscribing — they asked you in person, say — send{ "resubscribe": true }withPOST /api/v1/contactsorPATCH /api/v1/contacts/{id}: the contact becomessubscribedwithsubscribed_atset to now andsourceunchanged, and theunsubscribedentry is removed once the write succeeds.resubscribehas no effect on a bounced, complained or deleted address: those remain409.
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.
Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.