Suppressions
Manage your email suppression list. Suppressed email addresses are automatically blocked from receiving future emails — they are silently filtered out during sends and don't count against your quota.
The platform automatically adds addresses to the suppression list when emails bounce or receive complaints. You can also manually suppress or unsuppress addresses via the SDK.
All examples assume you've set the API key in the constructor. For multi-tenant usage, pass { apiKey } as the last argument to any method. See Configuration.
List Suppressions
const result = await outbound.suppressions.list({
reason: 'bounce',
page: 1,
limit: 50,
});
console.log(result.pagination.total); // 150 total suppressed emails
for (const s of result.suppressions) {
console.log(`${s.email} — ${s.reason} — ${s.created_at}`);
}Query Parameters
| Field | Type | Default | Description |
|---|---|---|---|
reason | string | All | Filter by reason: bounce, complaint, manual, unsubscribe |
search | string | All | Partial, case-insensitive match on the address. Comma-separated terms are OR'd together |
page | number | 1 | Page number (min: 1) |
limit | number | 50 | Items per page (min: 1, max: 100) |
search does substring matching, so one call can sweep several domains at once:
// Every suppressed address containing "acme.com" OR "gmail.com"
const result = await outbound.suppressions.list({ search: 'acme.com,gmail.com' });To look up known addresses exactly, use search() instead. It is index-backed and tells you which of your addresses were not found.
Response
{
suppressions: [
{
id: '550e8400-e29b-41d4-a716-446655440000',
tenant_id: 'uuid',
email: 'bounced-user@example.com',
reason: 'bounce',
created_at: '2026-03-08T10:00:00.000Z'
},
{
id: '660e8400-e29b-41d4-a716-446655440001',
tenant_id: 'uuid',
email: 'spam-reporter@example.com',
reason: 'complaint',
created_at: '2026-03-07T15:30:00.000Z'
}
],
pagination: {
page: 1,
limit: 50,
total: 150
}
}Possible Errors
| Error | Status | Cause |
|---|---|---|
AuthenticationError | 401 | Invalid or missing API key |
Auto-Pagination
Iterate through all suppressed emails without managing pages manually:
for await (const suppression of outbound.suppressions.listAll({ reason: 'bounce' })) {
console.log(`${suppression.email} — bounced on ${suppression.created_at}`);
}You can also pass a limit to control batch size (default: 50, max: 100):
for await (const suppression of outbound.suppressions.listAll({ limit: 100 })) {
// fetches 100 per page internally
}Address Validation
search(), bulkAdd() and bulkRemove() all take a list of addresses and all validate every address server-side, against the same RFC regex the send path uses, before anything is queried or written.
A malformed address never fails the whole call. Instead every response splits your input into two arrays:
| Field | Contents |
|---|---|
accepted | Valid addresses, lowercased and deduplicated. These were acted on |
rejected | { email, reason } for each address that failed validation. These were ignored |
{
accepted: ['a@example.com', 'b@example.com'],
rejected: [
{ email: 'not-an-email', reason: 'invalid_format' },
{ email: 'aaaa...@example.com', reason: 'too_long' },
],
}| Reason | Meaning |
|---|---|
invalid_format | Failed the RFC email regex. Bare addresses only: Name <a@b.com> is rejected, because a display name can contain a comma and would break the list |
too_long | Longer than 254 characters (the RFC 5321 limit) |
rejected[].email echoes the address exactly as you supplied it, so you can match it back to your source row. Every response also carries a summary object with the counts, where summary.requested is what you sent, before deduplication.
Passing addresses
All three methods accept emails as either a comma-separated string or an array. The SDK normalizes both:
await outbound.suppressions.bulkAdd({ emails: 'a@example.com, b@example.com' });
await outbound.suppressions.bulkAdd({ emails: ['a@example.com', 'b@example.com'] });Limits
search() accepts up to 100 addresses per request (it is a GET, so the list travels in the query string). bulkAdd() and bulkRemove() accept up to 1000. Exceeding the limit throws BadRequestError. The list is never silently truncated, because that would report success for addresses that were never touched.
Search Specific Addresses
Check whether specific addresses are suppressed. Exact match, case-insensitive, up to 100 per call. This is the "is this address blocked?" lookup; for substring matching use list({ search }).
const result = await outbound.suppressions.search({
emails: ['bounced@example.com', 'clean@example.com', 'not-an-email'],
});
console.log(result.suppressions); // the ones that ARE suppressed, newest first
console.log(result.notFound); // ['clean@example.com'] — valid, but not on the list
console.log(result.rejected); // [{ email: 'not-an-email', reason: 'invalid_format' }]
console.log(result.summary); // { requested: 3, accepted: 2, rejected: 1, found: 1, notFound: 1 }Parameters
| Field | Type | Required | Description |
|---|---|---|---|
emails | string | string[] | Yes | Addresses to look up (max 100 per request) |
Response
{
suppressions: [
{
id: '550e8400-e29b-41d4-a716-446655440000',
tenant_id: 'uuid',
email: 'bounced@example.com',
reason: 'bounce',
created_at: '2026-03-08T10:00:00.000Z'
}
],
accepted: ['bounced@example.com', 'clean@example.com'],
rejected: [{ email: 'not-an-email', reason: 'invalid_format' }],
notFound: ['clean@example.com'],
summary: { requested: 3, accepted: 2, rejected: 1, found: 1, notFound: 1 }
}Filtering a list before you send
const recipients = ['a@example.com', 'b@example.com', 'c@example.com'];
const { suppressions, rejected } = await outbound.suppressions.search({ emails: recipients });
const blocked = new Set(suppressions.map(s => s.email));
const sendable = recipients.filter(r => !blocked.has(r.toLowerCase()));
console.log(`${sendable.length} sendable, ${blocked.size} suppressed, ${rejected.length} malformed`);TIP
You do not need this to avoid sending to suppressed addresses: the platform already filters them at send time. Use it to show blocked addresses in your own UI, or to clean a list before import.
Possible Errors
| Error | Status | Cause |
|---|---|---|
BadRequestError | 400 | emails missing or empty, or more than 100 addresses supplied |
AuthenticationError | 401 | Invalid or missing API key |
Add Suppression
Manually add an email address to your suppression list. Future sends to this address will be silently filtered out.
const { suppression } = await outbound.suppressions.add({
email: 'bad-address@example.com',
reason: 'manual',
});
console.log(suppression.id); // 'uuid'
console.log(suppression.email); // 'bad-address@example.com'
console.log(suppression.reason); // 'manual'Parameters
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address to suppress (auto-lowercased) |
reason | string | No | One of: bounce, complaint, manual, unsubscribe (default: manual) |
Response
{
suppression: {
id: '550e8400-e29b-41d4-a716-446655440000',
tenant_id: 'uuid',
email: 'bad-address@example.com',
reason: 'manual',
created_at: '2026-03-08T10:00:00.000Z'
}
}Idempotent
If the email is already on your suppression list, the existing record is returned — no duplicate is created, no error is thrown.
Possible Errors
| Error | Status | Cause |
|---|---|---|
BadRequestError | 400 | Missing email, malformed email, or unknown reason |
AuthenticationError | 401 | Invalid or missing API key |
Bulk Add Suppressions
Suppress up to 1000 addresses in one request.
const result = await outbound.suppressions.bulkAdd({
emails: ['a@example.com', 'b@example.com', 'not-an-email'],
reason: 'manual',
});
console.log(result.added); // ['a@example.com'] — newly suppressed
console.log(result.alreadySuppressed); // ['b@example.com'] — was already on the list
console.log(result.rejected); // [{ email: 'not-an-email', reason: 'invalid_format' }]
console.log(result.summary); // { requested: 3, accepted: 2, rejected: 1, added: 1, alreadySuppressed: 1 }Parameters
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
emails | string | string[] | Yes | - | Addresses to suppress (max 1000 per request) |
reason | string | No | manual | One of: bounce, complaint, manual, unsubscribe |
Response
{
suppressions: [ /* the record for EVERY accepted address, new and pre-existing */ ],
accepted: ['a@example.com', 'b@example.com'],
rejected: [{ email: 'not-an-email', reason: 'invalid_format' }],
added: ['a@example.com'],
alreadySuppressed: ['b@example.com'],
summary: { requested: 3, accepted: 2, rejected: 1, added: 1, alreadySuppressed: 1 }
}Idempotent
Addresses already on your list are reported in alreadySuppressed and their existing record, including its original reason, is left untouched. Re-running the same bulkAdd() is safe: nothing is duplicated and nothing is overwritten.
Partial results return 200
The call succeeds even when some or all addresses were rejected. The accepted / rejected split is the answer, so you do not need one code path for "all bad" and another for "some bad". A BadRequestError means the request was unusable (no emails at all, over the limit, unknown reason), not that an address was invalid.
Importing a suppression list
const CHUNK = 1000; // the per-request maximum
for (let i = 0; i < allEmails.length; i += CHUNK) {
const result = await outbound.suppressions.bulkAdd({
emails: allEmails.slice(i, i + CHUNK),
reason: 'manual',
});
console.log(`${result.summary.added} added, ${result.summary.alreadySuppressed} already there`);
for (const bad of result.rejected) {
console.warn(`skipped ${bad.email}: ${bad.reason}`);
}
}Possible Errors
| Error | Status | Cause |
|---|---|---|
BadRequestError | 400 | emails missing or empty, more than 1000 addresses, or unknown reason |
AuthenticationError | 401 | Invalid or missing API key |
Remove Suppression
Remove an email address from your suppression list, allowing future sends to this address.
const result = await outbound.suppressions.remove('bad-address@example.com');
// { message: 'Suppression removed' }The email parameter is URL-encoded automatically by the SDK.
Possible Errors
| Error | Status | Cause |
|---|---|---|
NotFoundError | 404 | Email is not on your suppression list |
AuthenticationError | 401 | Invalid or missing API key |
WARNING
Removing a bounced or complained email from your suppression list means future sends to that address will go through. Only do this if you're confident the issue has been resolved (e.g., the recipient fixed their mailbox).
Bulk Remove Suppressions
Unsuppress up to 1000 addresses in one request.
const result = await outbound.suppressions.bulkRemove({
emails: 'a@example.com, never-suppressed@example.com, not-an-email',
});
console.log(result.removed); // ['a@example.com'] — actually removed
console.log(result.notFound); // ['never-suppressed@example.com'] — valid, but was not on the list
console.log(result.rejected); // [{ email: 'not-an-email', reason: 'invalid_format' }]
console.log(result.summary); // { requested: 3, accepted: 2, rejected: 1, removed: 1, notFound: 1 }Parameters
| Field | Type | Required | Description |
|---|---|---|---|
emails | string | string[] | Yes | Addresses to unsuppress (max 1000 per request) |
Response
{
accepted: ['a@example.com', 'never-suppressed@example.com'],
rejected: [{ email: 'not-an-email', reason: 'invalid_format' }],
removed: ['a@example.com'],
notFound: ['never-suppressed@example.com'],
summary: { requested: 3, accepted: 2, rejected: 1, removed: 1, notFound: 1 }
}Missing addresses are not an error
Unlike remove(), which throws NotFoundError for an address that is not on the list, bulkRemove() reports it in notFound. One stale entry cannot fail the whole batch.
WARNING
The same caution as remove() applies, multiplied: unsuppressing bounced or complained addresses in bulk means your next campaign will attempt all of them again. Repeatedly sending to addresses that hard-bounce damages your sending reputation. Filter by reason first if you only mean to clear manual entries:
const manual = [];
for await (const s of outbound.suppressions.listAll({ reason: 'manual' })) {
manual.push(s.email);
}
await outbound.suppressions.bulkRemove({ emails: manual });Possible Errors
| Error | Status | Cause |
|---|---|---|
BadRequestError | 400 | emails missing or empty, or more than 1000 addresses supplied |
AuthenticationError | 401 | Invalid or missing API key |
Suppression Reasons
| Reason | Description | Added By |
|---|---|---|
bounce | Email bounced (invalid address, full mailbox, etc.) | Platform (automatic) |
complaint | Recipient reported the email as spam | Platform (automatic) |
manual | Manually added via SDK or dashboard | You |
unsubscribe | Recipient clicked an unsubscribe link | Platform (automatic) |
How Suppression Affects Sending
When you send an email (single, bulk, or template-based), the platform checks each recipient against your suppression list before sending:
- Single send (
outbound.email.send(...)) — if the recipient is suppressed, the email is silently skipped - Bulk send (
outbound.email.bulk(...)) — suppressed recipients are removed from the batch and reported in the response:ts{ recipientCount: 485, // emails that will be sent suppressedCount: 12, // emails filtered out suppressedEmails: ['bounced@example.com', 'complained@example.com'] } - Template bulk send (
outbound.templates.bulkSend(...)) — same behavior as bulk send
Suppressed emails do not count against your sending quota.
When a change takes effect
Immediately. Suppressions are written synchronously, so an address suppressed by add() or bulkAdd() is filtered out of the very next send once the call returns, and one removed by remove() or bulkRemove() is sendable again just as fast. Mirroring the change to the underlying provider happens in the background and does not gate sending.
Complete Example: Managing Suppressions
import { Outbound, NotFoundError } from '@masters-union/outbound-sdk';
const outbound = new Outbound({ apiKey: 'mu_outbound_...' });
// 1. Check current suppressions
const result = await outbound.suppressions.list();
console.log(`${result.pagination.total} emails suppressed`);
// 2. Review bounces
for await (const s of outbound.suppressions.listAll({ reason: 'bounce' })) {
console.log(`Bounced: ${s.email} on ${s.created_at}`);
}
// 3. Manually suppress an email
await outbound.suppressions.add({
email: 'do-not-email@example.com',
reason: 'manual',
});
// 4. Import a whole opt-out list in one call, validated server-side
const imported = await outbound.suppressions.bulkAdd({
emails: ['optout1@example.com', 'optout2@example.com', 'typo@@example.com'],
reason: 'manual',
});
console.log(`${imported.summary.added} added, ${imported.summary.rejected} malformed`);
for (const bad of imported.rejected) {
console.warn(`skipped ${bad.email}: ${bad.reason}`);
}
// 5. Check specific addresses before building a campaign list
const check = await outbound.suppressions.search({
emails: ['optout1@example.com', 'active@example.com'],
});
console.log('blocked:', check.suppressions.map(s => s.email));
console.log('clear:', check.notFound);
// 6. Remove a suppression (e.g., user re-verified their email)
try {
await outbound.suppressions.remove('do-not-email@example.com');
console.log('Suppression removed, email can receive messages again');
} catch (error) {
if (error instanceof NotFoundError) {
console.log('Email was not on the suppression list');
}
}
// 7. Or unsuppress many at once: missing addresses are reported, not thrown
const cleared = await outbound.suppressions.bulkRemove({
emails: ['optout1@example.com', 'was-never-suppressed@example.com'],
});
console.log(`${cleared.summary.removed} removed, ${cleared.summary.notFound} were not listed`);
// 8. Bulk send, suppressed emails are auto-filtered
const bulkResult = await outbound.email.bulk({
fromEmail: 'noreply@yourcompany.com',
emailSubject: 'Monthly Update',
emails: allRecipients.map(r => ({
toEmail: r.email,
htmlBody: `<h1>Hi ${r.name}</h1>`,
})),
});
console.log(`Sent: ${bulkResult.recipientCount}`);
console.log(`Suppressed: ${bulkResult.suppressedCount}`);
console.log(`Suppressed emails:`, bulkResult.suppressedEmails);