Skip to content

Email suppression list: what goes on it, why it matters, and how to manage it over the API

Published Updated 11 min readBy the Rasket team

A list shape with one line struck through beside a single envelope, drawn as white and violet outlines on black.

What an email suppression list is

An email suppression list is the set of addresses a sending system refuses to send to, checked before every message goes out. Every address on it got there because something happened: a mailbox rejected a message permanently, a recipient pressed the spam button, somebody unsubscribed, or an operator decided. The list is the memory of those events, and it is what stops a system from repeating a mistake a mailbox provider has already told it about.

It is easy to mistake for a courtesy. It is closer to a safety interlock. Mailbox providers score senders on how often their mail bounces and how often it is reported, and both scores are driven by exactly the addresses a suppression list holds. A sender without one keeps hitting dead mailboxes and irritated people, and the score goes the only way it can. The list is also evidence: it is the record that an unsubscribe was honoured, which matters when someone asks.

On Rasket the list is per team. It applies to every domain and every API key the team owns, to transactional sends and broadcasts alike, and it is written automatically by the events that should write it. The glossary entry has the short version; the rest of this post is what goes on it, what it protects, and how to manage it from code.

What goes on it

Every row carries an origin, which says why it exists, and where an email caused it, a source_id naming that email. There are four origins.

  • bounce — a hard bounce: the receiving server said the address does not exist or will never accept mail, typically with a 5xx reply. Added automatically when the bounce arrives. Soft bounces, a full mailbox or a temporary refusal, do not add a row; the bounce post draws the line.
  • complaint — the recipient reported the message as spam and their provider passed that on through a feedback loop. Added automatically. A complaint is the strongest signal on the list, because the person has told their provider directly that they did not want the mail.
  • unsubscribe — the recipient used an unsubscribe link or the one-click header that RFC 8058 defines and every broadcast carries. Their choice is also written to the contact’s consent ledger, so a broadcast skips them for two reasons.
  • manual — you added it, one at a time or in a batch. A customer who asked support to stop all mail, an address from a previous provider’s list, a role account you know should never receive anything. source_id is empty for these.

The source_id is the useful part when a customer asks why they stopped getting receipts. Fetch the suppression by address, follow source_id to the email that bounced, and you have the date, the subject and the diagnostic the receiving server gave.

How it protects your reputation

Mailbox providers watch two rates above all others. Bounce rate is the share of your messages that were rejected outright, and a high one says your list is old or was never yours. Complaint rate is the share reported as spam, and a high one says people did not ask for what you sent. Google’s sender guidelines put a number on the second: keep the spam rate reported in Postmaster Tools below 0.1%, and never let it reach 0.3%. There is no published threshold for bounces, but a sender that keeps retrying addresses it has been told are dead looks, to a filter, much like one that bought a list.

You can watch both numbers yourself. Google’s Postmaster Tools reports the spam rate it sees for your domain, and your own events give the rest: count email.bounced and email.complained against email.sent over the same window, per domain, and put the two ratios on a dashboard. The complaint rate entry explains how the second one is measured and why a small denominator makes it jumpy.

The suppression list is the mechanism that keeps both rates from compounding. One hard bounce is information. The same address bouncing every week is a pattern that shows up in your domain’s reputation. One complaint is a person’s opinion. Sending to them again after they complained is what turns an opinion into a score.

The three kinds of suppression compared
Bounce-drivenComplaint-drivenManual
Added byThe bounce, automatically, at the moment it arrives.The feedback loop, automatically.You, over the API or in the dashboard.
What it saysThe mailbox cannot take mail.The person does not want your mail.Whatever reason you had; the row does not record it.
Safe to remove?Only when you know the mailbox is back, such as a typo the customer corrected.Almost never. Ask the person, and only remove it if they asked you to.When the reason is gone. It was your decision to begin with.
Cost of ignoring itBounce rate climbs; blocks follow.Complaint rate climbs; spam placement follows, and it is slow to recover.Depends on why it was added. Often a promise to a customer.
Best forKeeping dead addresses from being retried, forever, without a human in the loop.Honouring the one signal a mailbox provider treats as the person's own words.Everything the automatic signals cannot know: support requests, migrations, policy.

The row worth a second look is “Safe to remove?”. The API lets you delete any suppression, and there are legitimate reasons to. But removing a bounce-driven row sends to an address that has already rejected you once, and removing a complaint-driven one sends to a person who reported you. Treat the delete as a decision with a name attached, not a cleanup.

Managing it over the API

Six operations cover it: add one, list, look one up, remove one, and add or remove up to 100 in a batch. The suppressions reference has every field and response; this is the shape of each.

curl -X POST https://api.rasket.com/suppressions \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0" \  -H "Content-Type: application/json" \  -d '{ "email": "ronald.williams@example.com" }'

That answers 201 with the new row’s id, and the row has origin: manual. Listing takes origin, a case-insensitive search over the address, a date range, and cursor pagination with has_more; rows come back newest first.

# Every bounce-driven suppression, newest firstcurl "https://api.rasket.com/suppressions?origin=bounce&limit=20" \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0"
# May I send to this person? 200 with the row, or 404curl https://api.rasket.com/suppressions/ronald.williams@example.com \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0"
# Allow sending againcurl -X DELETE https://api.rasket.com/suppressions/ronald.williams@example.com \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0"

The second request is the one to build into an application. Passing an address to GET /suppressions/{idOrAddress} asks “may I send to this person?” and answers with the row or a 404, without listing anything. A preferences page that shows a customer whether they are blocked, and why, is that one request and the origin field.

# Up to 100 addresses per requestcurl -X POST https://api.rasket.com/suppressions/batch/add \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0" \  -H "Content-Type: application/json" \  -d '{ "emails": ["ronald.williams@example.com", "ada@example.com"] }'
# Remove by address (or by id: { "ids": [...] }, never both)curl -X POST https://api.rasket.com/suppressions/batch/remove \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0" \  -H "Content-Type: application/json" \  -d '{ "emails": ["ada@example.com"] }'

Two rules on batches. Each takes at most 100 addresses. And batch/remove takes either emails or ids: sending both, or neither, is a 422 invalid_parameter. An address already suppressed is left as it is by batch/add, and one that is not suppressed is skipped by batch/remove, so both are safe to run twice.

import { Rasket } from "rasket";
const rasket = new Rasket({  apiKey: process.env.RASKET_API_KEY,  userAgent: "acme-billing/1.0",});
const { body } = await rasket.suppressions.batch.add({  emails: ["ronald.williams@example.com", "ada@example.com"],});
console.log(body.data.length); // 2

The SDK mirrors the six calls: suppressions.create, suppressions.get, suppressions.remove, and suppressions.batch.add and suppressions.batch.remove. Every response and every error carries rateLimit, which the import loop below reads on a 429.

Suppressed sends still answer 200

This surprises people the first time. Send to a suppressed address and the request succeeds: 200, an id, exactly as if the message were on its way. The message is accepted and durably recorded, then stopped rather than delivered. The record shows what happened.

curl -X POST https://api.rasket.com/emails \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0" \  -H "Idempotency-Key: order-1042-shipped" \  -H "Content-Type: application/json" \  -d '{    "from": "Acme <orders@send.acme.example>",    "to": ["ronald.williams@example.com"],    "subject": "Your order has shipped",    "text": "Order 1042 is on its way."  }'# 200 { "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c" }
curl https://api.rasket.com/emails/4ef9a417-02e9-4d39-ad75-9611e0fcc33c \  -H "Authorization: Bearer $RASKET_API_KEY" \  -H "User-Agent: acme-billing/1.0"# 200 { ..., "last_event": "suppressed" }

Two things tell you. GET /emails/{id} reads last_event as suppressed, the furthest state the message reached. And a webhook event named email.suppressed arrives, carrying the same fields every email.* event does plus a suppressed block with the reason.

{  "type": "email.suppressed",  "created_at": "2026-09-16T10:16:44.902Z",  "data": {    "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",    "from": "Acme <orders@send.acme.example>",    "to": ["ronald.williams@example.com"],    "subject": "Your order has shipped",    "suppressed": {      "reason": "previous_bounce",      "message": "The address is on this team's suppression list."    }  }}

Why not refuse with a 4xx?

Because the alternative is worse in every case that matters. A send is usually one step in a larger action: the order was placed, the receipt is being sent. If the API refused with 4xx, every caller would need a branch for “the order succeeded but the receipt request failed, and that is fine”, and most would not write it, so the order flow would break for a customer whose old address bounced once. Accepting the request keeps the caller’s code identical for every recipient and moves the outcome to where outcomes already live: the events. A suppressed send is never counted as delivered, and it never reaches the mailbox provider.

If you want to know before you send, ask: the lookup by address is one request. The usual pattern is to check at the point where a customer changes their email, and to let the send path stay simple.

Importing a list from another provider

Moving providers without moving the suppression list is the classic way to burn a new domain in its first week: every address that bounced or complained under the old setup is sent to again, from an identity with no history to absorb it. Import the list first, then send.

  • Export the addresses. Most providers offer an export of bounces, complaints and unsubscribes. One address per line is enough; the origin is lost, since imported rows are manual, so keep the original export if you may need to distinguish them later.
  • Batch by 100. Each batch/add takes at most 100 addresses. Keep the file’s order and walk it in chunks, so a resumed import continues from where it stopped rather than starting over.
  • Mind the rate limit. The API allows ten requests a second per team, so the ceiling is 1,000 addresses a second; a list of 100,000 takes under two minutes. Read retry-after on a 429 and wait that long. The rate limits guide has the header set.
  • Run it twice if you have to. An address already suppressed is left as it is, so re-running the whole file after a crash is safe. Idempotency here comes from the data, not from a key.
import { readFile } from "node:fs/promises";import { Rasket, RasketApiError } from "rasket";
const rasket = new Rasket({  apiKey: process.env.RASKET_API_KEY,  userAgent: "acme-billing/1.0",});
// One address per line, exported from the previous provider.const emails = (await readFile("suppressions.txt", "utf8"))  .split("\n")  .map((line) => line.trim())  .filter(Boolean);
for (let i = 0; i < emails.length; i += 100) {  const chunk = emails.slice(i, i + 100);  try {    await rasket.suppressions.batch.add({ emails: chunk });  } catch (error) {    if (error instanceof RasketApiError && error.statusCode === 429) {      const wait = error.rateLimit?.retryAfterSeconds ?? 1;      await new Promise((resolve) => setTimeout(resolve, wait * 1000));      i -= 100; // retry the same chunk      continue;    }    throw error;  }}

Import before the first send, not after

The list is consulted when a message is sent, so a row that is not there yet protects nothing. For a migration that means: domain verified, list imported, then the first send. The other order works too, just once, and at the cost of the addresses you were moving to avoid.

One more thing the import should not carry over: soft bounces. If the old provider exported them alongside hard bounces, filter them out, because a full mailbox in March is not a reason to never write to someone again. What belongs on the list is the set of addresses that said no, permanently or personally. Broadcasts honour the same list, so one import covers both kinds of mail.

Frequently asked questions

Does the suppression list apply to transactional email as well as broadcasts?

Yes. The list is per team and is consulted before every send, whichever endpoint made it. A hard bounce from a receipt suppresses the address for newsletters too, and an unsubscribe from a newsletter is written to the contact's consent ledger as well as to the list. Broadcasts additionally skip contacts who opted out of the topic or unsubscribed globally.

Why does a send to a suppressed address return 200 instead of an error?

Because the send is accepted and recorded; it is the delivery that does not happen. Refusing with a 4xx would make every caller handle a case most would ignore, and an order flow would fail for a customer whose old address once bounced. The outcome lives where outcomes already live: last_event reads suppressed and an email.suppressed webhook event arrives.

Can I find out whether an address is suppressed before I send?

Yes. GET /suppressions/{idOrAddress} takes the address itself and answers with the row, including its origin and the source_id of the email that caused it, or a 404 when the address is clear. It is one request, and it is the right thing to call when a customer changes their email or asks why they stopped receiving mail.

Should I ever remove a suppression?

Sometimes, and always deliberately. A manual row can go when the reason for it has gone. A bounce-driven row can go when you know the mailbox is back, such as a typo the customer has corrected. A complaint-driven row should stay unless the person themselves asked you to resume, because sending to someone who reported you is the fastest way to a reputation problem.

How fast can I import a list from another provider?

Each batch/add request takes up to 100 addresses and the API allows ten requests a second per team, so the ceiling is 1,000 addresses a second, or a 100,000-row list in under two minutes. Read retry-after on a 429 and wait that long. Addresses already suppressed are left as they are, so re-running the file after a crash is safe.

Do soft bounces go on the suppression list?

No. A soft bounce is a temporary refusal, such as a full mailbox or a server asking you to try later, and the address may well accept mail tomorrow. Only a hard bounce, where the receiving server says the address does not exist or will never accept mail, adds a row automatically. If a previous provider's export mixes the two, filter the soft bounces out before importing.

Sources

  1. M3AAWG Sender Best Common PracticesMessaging, Malware and Mobile Anti-Abuse Working Group, read 2026-09-16
  2. RFC 8058: Signaling One-Click Functionality for List Email HeadersIETF, read 2026-09-16
  3. Email sender guidelinesGoogle, read 2026-09-16
  • SuppressionsAddresses we will not send to, and why.
  • Suppression listA suppression list is the set of addresses you must not send to: hard bounces, spam complaints, and anyone who has unsubscribed.
  • Hard bounce vs soft bounce: what to do with eachA hard bounce is permanent, a soft bounce is temporary. What causes each, what the reply code tells you, when to retry, and when to suppress an address.
  • Spam complaint rateThe complaint rate is the share of your delivered messages that readers marked as spam, measured per mailbox provider rather than across your whole audience.
  • BroadcastsAn audience you own: contacts with typed properties, segments, topics people subscribe to, and broadcasts sent through the same pipeline as your other mail.

Start sending this morning

Sign up, verify a domain and send your first email in minutes.