Skip to content

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

ts
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

FieldTypeDefaultDescription
reasonstringAllFilter by reason: bounce, complaint, manual, unsubscribe
searchstringAllPartial, case-insensitive match on the address. Comma-separated terms are OR'd together
pagenumber1Page number (min: 1)
limitnumber50Items per page (min: 1, max: 100)

search does substring matching, so one call can sweep several domains at once:

ts
// 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

ts
{
  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

ErrorStatusCause
AuthenticationError401Invalid or missing API key

Auto-Pagination

Iterate through all suppressed emails without managing pages manually:

ts
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):

ts
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:

FieldContents
acceptedValid addresses, lowercased and deduplicated. These were acted on
rejected{ email, reason } for each address that failed validation. These were ignored
ts
{
  accepted: ['a@example.com', 'b@example.com'],
  rejected: [
    { email: 'not-an-email', reason: 'invalid_format' },
    { email: 'aaaa...@example.com', reason: 'too_long' },
  ],
}
ReasonMeaning
invalid_formatFailed 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_longLonger 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:

ts
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 }).

ts
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

FieldTypeRequiredDescription
emailsstring | string[]YesAddresses to look up (max 100 per request)

Response

ts
{
  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

ts
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

ErrorStatusCause
BadRequestError400emails missing or empty, or more than 100 addresses supplied
AuthenticationError401Invalid 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.

ts
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

FieldTypeRequiredDescription
emailstringYesEmail address to suppress (auto-lowercased)
reasonstringNoOne of: bounce, complaint, manual, unsubscribe (default: manual)

Response

ts
{
  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

ErrorStatusCause
BadRequestError400Missing email, malformed email, or unknown reason
AuthenticationError401Invalid or missing API key

Bulk Add Suppressions

Suppress up to 1000 addresses in one request.

ts
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

FieldTypeRequiredDefaultDescription
emailsstring | string[]Yes-Addresses to suppress (max 1000 per request)
reasonstringNomanualOne of: bounce, complaint, manual, unsubscribe

Response

ts
{
  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

ts
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

ErrorStatusCause
BadRequestError400emails missing or empty, more than 1000 addresses, or unknown reason
AuthenticationError401Invalid or missing API key

Remove Suppression

Remove an email address from your suppression list, allowing future sends to this address.

ts
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

ErrorStatusCause
NotFoundError404Email is not on your suppression list
AuthenticationError401Invalid 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.

ts
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

FieldTypeRequiredDescription
emailsstring | string[]YesAddresses to unsuppress (max 1000 per request)

Response

ts
{
  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:

ts
const manual = [];
for await (const s of outbound.suppressions.listAll({ reason: 'manual' })) {
  manual.push(s.email);
}
await outbound.suppressions.bulkRemove({ emails: manual });

Possible Errors

ErrorStatusCause
BadRequestError400emails missing or empty, or more than 1000 addresses supplied
AuthenticationError401Invalid or missing API key

Suppression Reasons

ReasonDescriptionAdded By
bounceEmail bounced (invalid address, full mailbox, etc.)Platform (automatic)
complaintRecipient reported the email as spamPlatform (automatic)
manualManually added via SDK or dashboardYou
unsubscribeRecipient clicked an unsubscribe linkPlatform (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

ts
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);

Released under the MIT License.