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-addressinga+foo@example.com and a@example.com are 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_ERROR in your batch response.
  • Don't cache the suppression list long. It changes every time a user unsubscribes. Re-scrub for each blast.