Suppression and opt-outs
The suppression list is your do-not-contact register. Any identifier on it (a phone number or email address) is silently skipped at send time — for both single sends and campaigns — so you never message someone who has opted out.
Why it matters (POPIA)
South Africa's POPIA requires that you honour opt-outs and stop contacting people who have withdrawn consent. Suppression makes that the default: once an identifier is suppressed, Blueticked enforces it on every send across the workspace, so compliance does not depend on each integration remembering to check.
STOP auto-suppresses
When a customer replies STOP (or another recognised opt-out keyword) on WhatsApp or SMS, Blueticked adds them to the suppression list automatically with a reason of stop_keyword. Hard bounces and spam complaints on email suppress automatically too, as bounce and spam_report. You do not need to wire any of this up — it happens server-side.
List entries
Cursor-paginated, newest first. Optionally filter by channel:
const page = await bt.suppression.list({ limit: 100 });
for (const entry of page.data) {
// entry.identifier, entry.channel, entry.reason, entry.created_at
console.log(entry.identifier, entry.reason);
}
// Only email opt-outs:
const emailOptOuts = await bt.suppression.list({ channel: "email" });Add an opt-out
Add an entry yourself when someone opts out through your own channels (a CRM, a call centre, an unsubscribe form). Use channel: "all" to block every channel, or a specific channel to opt out of just that one. It is idempotent — re-adding the same identifier returns the existing entry:
const entry = await bt.suppression.add({
identifier: "+27821234567",
channel: "all",
reason: "manual",
reason_detail: "Opted out via CRM",
});
console.log(entry.id); // a UUID: "9f1c2a30-8b4e-4d6a-9a11-2f7c5b9e0d33"Valid reason values are manual, stop_keyword, bounce, and complaint; for your own additions use manual. Two of them are normalised on write — manual is stored as manual_add and complaint as spam_report — so the reason you read back can differ from the one you sent. Compare against the stored value, not the one you posted.
Remove an entry
Removing an entry makes that identifier contactable again, so removal is deliberately not symmetric with adding. What happens depends on why the entry exists, and for most real entries a bare remove() is refused:
| Stored reason | Removal |
|---|---|
bounce, spam_report | 403 forbidden, permanently. These mark mailboxes that rejected or reported this sender; resuming sends to them damages the whole workspace's deliverability. No header lifts this — a person removes it in the dashboard or not at all. |
stop_keyword, unsubscribe_link, preference_center | The person asked to stop, so the API needs you to say you have their re-consent: pass confirmReconsent (the X-Confirm-Reconsent: true header). Without it, 400 validation_failed. |
manual_add, csv_import | Deletes outright — no confirmation header needed. |
// An entry you added yourself deletes outright:
await bt.suppression.remove(entry.id);
// A STOP / unsubscribe / preference-centre entry needs explicit re-consent.
// Only set this once the recipient has actually asked to hear from you again:
await bt.suppression.remove(entry.id, { confirmReconsent: true });
// Raw HTTP is the same header:
// DELETE /api/v1/suppression/{id}
// X-Confirm-Reconsent: trueThe id is the entry's UUID, from list() or add(). Anything that is not a UUID is rejected with 400 validation_failed before the lookup runs. Read the stored reason first and branch on it — that is the only way to know which of the three rows above you are in.
Suppression vs. the contact gate
The suppression list is keyed by identifier (a phone or email), so it applies even before a contact exists. A contact also has its own suppressed flag (see Contacts) that blocks every channel for that one contact record. Both are checked at send time; either one being set is enough to skip the send.