Documentation
Sign inGet started

Suppressions

Every workspace owns a suppression list: a set of email addresses we will not deliver to. Hard bounces, spam complaints, and unsubscribes land on it on their own, and you can add addresses yourself. Repeatedly mailing addresses that bounce or report spam is the fastest way to get your domain blocked by mailbox providers, so we stop those sends before they leave the platform.
Browse and manage the list under Email → Suppressions in the dashboard, through the suppressions API, or with bird email suppressions from the terminal.
The Suppressions page in the dashboard, listing suppressed addresses with their reason, origin, and creation date, above a Create suppression button

The four reasons and what they block

Each record has a reason saying why the address is listed, and an applies_to policy controlling which categories it blocks:
Reasonapplies_toMarketing categoryTransactional category
hard_bounceallBlockedBlocked
complaintnon_transactionalBlockedAllowed
unsubscribenon_transactionalBlockedAllowed
manualallBlockedBlocked
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.
  • complaint and unsubscribe: preference signals. Someone who reported your newsletter as spam may still need a password reset or an order confirmation, so these block only non-transactional sends.
  • manual: a deliberate decision by you or your team. We do not second-guess it, so a manual suppression blocks every category, including transactional.
An address holds one record per reason, so a hard bounce and an earlier unsubscribe sit side by side as separate records, and delivery stays blocked while any blocking record remains. We fail closed on anything we do not recognize: if a record comes back with an applies_to your integration has never seen, treat it as blocking every category, which is how we treat it ourselves.

How addresses get added automatically

We add suppressions in response to recipient signals, so a bounce or complaint needs no action from you:
TriggerResulting 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, and the classification table shows which bounce_class values count as hard. Two outcomes that look like failures leave the address sendable:
  • Soft bounces and deferrals (email.deferred, or email.bounced with bounce_type: "soft"): transient failures like a full mailbox. We retry.
  • Send-side rejections: generation failures and policy rejections are problems with the send rather than the address. They produce email.rejected events and no suppression.
Repeat signals for an address that is already suppressed for the same reason leave the original record alone, including its created_at. The record keeps source_email_id and source_recipient_id, which link an automatic suppression back to the exact message and recipient that caused it. Those two fields answer the support question "why did this person stop getting our email", and they are null on manual additions.
Every addition, automatic or manual, fires an email_suppression.created event to your webhook endpoint with the suppression_id, the suppressed email, the reason, and the workspace_id, so your own system can mirror the list without polling:
Contoh kode
{
  "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 adds, lists, looks up, and deletes single records. Addresses are lowercased before storage and lookup, and they never appear in a URL path, because a path lands in access logs and an email address is personal data. To find the record for an address, filter the list with ?email=.
The SDK tabs below reach suppressions through each client's raw-request method, which carries the same auth, retries, and base-URL handling as a typed call; the response shape is the one you declare.

Add an address

type Suppression = { id: string; email: string; reason: string };

const suppression = await bird.request<Suppression>({
  method: "POST",
  path: "/v1/email/suppressions",
  body: { email: "user@example.com" },
});
On the CLI, bird email suppressions covers list and remove; adding an address goes through the API.
Manual additions get reason: manual and applies_to: all, so they block every category. The call is idempotent: a new suppression returns 201 Created, and an address already manually suppressed returns 200 OK with the existing record rather than a conflict. Either way the body is the suppression object:
Contoh kode
{
  "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 record came to exist. Manual additions get api_key or user, depending on whether the caller authenticated with an API key or a dashboard session. Automatic additions get bounce_event, complaint_event, or unsubscribe_event, and an opt-out through our own unsubscribe page gets unsubscribe_link.

List and look up

type Suppressions = { data: Array<{ id: string; email: string; reason: string }> };

const suppressions = await bird.request<Suppressions>({
  method: "GET",
  path: "/v1/email/suppressions?limit=25",
});
console.log(suppressions.data.length);
The list is cursor-paginated, newest first, and filterable by reason. To check one address, pass it as the email query parameter:
const suppressions = await bird.request({
  method: "GET",
  path: "/v1/email/suppressions?email=user@example.com",
});
console.log(suppressions.data.length);
An empty data array means the address is not suppressed, and several records come back when more than one reason applies. The email filter matches case-insensitively by prefix, so a complete address returns that address's records and a fragment such as alice returns every suppressed address starting with it.

Remove an address

await bird.request({
  method: "DELETE",
  path: "/v1/email/suppressions/sup_01ky7qckqrf06r38g49b9kxdbc",
});
Returns 204 No Content. The delete is permanent: we retain nothing, and the address becomes sendable again. Deleting by address takes two calls, a ?email= lookup for the ID and then the delete, and an address suppressed for several reasons needs each blocking record deleted. Be deliberate about removing a hard_bounce record, because an address that still does not exist bounces on the next send and re-suppresses itself.

What happens when you send to a suppressed address

We reject the recipient where you can see it. The recipient gets a recipient_id and appears in the message's recipient list with status rejected, an email.rejected event is recorded for it with rejection_reason: "recipient_suppressed" in the events API and your webhooks, and the rest of the recipients deliver normally.
The message itself is still accepted with a 202, even when every one of its recipients is suppressed. We resolve suppression after accepting the send, while we process the message, so an address you add now applies within a few minutes and never stops a send already in flight.

Testing with the sandbox

The testing sandbox exercises suppression handling deterministically. Sending to suppressed@messagebird.dev behaves as though the address were on your list: the recipient is rejected with rejection_reason: "recipient_suppressed" and never reaches delivery. The sandbox bounce and complaint addresses (bounce@messagebird.dev, complaint@messagebird.dev) run their outcomes through the real event pipeline while writing nothing to your suppression list, so the same test addresses stay reusable across runs.

Next steps