Scrub a recipient list before send
Before a large blast, filter addresses against your suppression list client-side. Avoids round-trips, saves quota, gives you the "dropped" count upfront.
Via the API
const API = "https://api.sendoka.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.SENDOKA_API_KEY}` };
async function scrub(addresses: string[]): Promise<{ keep: string[]; drop: string[] }> {
// Paginate the full suppression list (email channel only)
const suppressed = new Set<string>();
let cursor: string | null = null;
do {
const url = new URL(`${API}/suppressions`);
url.searchParams.set("channel", "email");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers });
const page = await res.json();
for (const row of page.data) suppressed.add(row.address.toLowerCase());
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
const keep: string[] = [];
const drop: string[] = [];
for (const a of addresses) {
(suppressed.has(a.toLowerCase()) ? drop : keep).push(a);
}
return { keep, drop };
}
Or just let the audience endpoint do it
POST /api/v1/audiences/:id/send already drops suppressed addresses before insert. Response tells you the count:
{ "queued": 23417, "skipped_suppressed": 112 }
Use client-side scrub only when you need the list of dropped addresses (to remove them from your own DB) or when you're sending via /emails/batch instead of audiences.
Gotchas
- Case-insensitive — suppressions are stored lowercase; match accordingly.
- Plus-addressing —
a+foo@example.comanda@example.comare different suppressions. Treat as-written. - Race condition — between scrub and send, a new bounce could auto-add an address. Sendoka will still block it at send time, so worst case you see a per-row
VALIDATION_ERRORin your batch response. - Don't cache the suppression list long. It changes every time a user unsubscribes. Re-scrub for each blast.