Opt-Out & Suppression Lists
A suppression list is the set of addresses you must never message
again — people who replied STOP, unsubscribed, bounced, or complained.
Honouring it is a legal requirement on every regulated channel, and
Orbit treats it as a hard send-gate: a suppressed address is dropped
before dispatch regardless of campaign, contact import, or API call.
This page covers how suppression works and how to bulk-import an
existing suppression list — for example when migrating from another
platform — through a single CSV upload.
All endpoints below are rooted at
https://api.orbit.devotel.io/api/v1/compliance.
How suppression happens
An address lands on the suppression list in several ways:
- A contact replies with a STOP keyword on SMS/WhatsApp.
- A contact opts out via the Preference Center.
- You record an opt-out through the
Consent API (
opt_in: false).
- You bulk-import a list (this page).
Each entry has a channel scope. Phone and WhatsApp opt-outs default
to scope all — a STOP signal on a phone number suppresses every
channel reachable on that number — while email opt-outs are scoped to
email. The full scope set is: all, sms, voice, whatsapp,
email, push, telegram, messenger, rcs.
Phone-based suppressions are mirrored to the DNC list and the
matching contacts are flagged, so a suppressed number is honoured by
voice/dialer gates as well as messaging.
Bulk CSV import
POST /compliance/suppression-list/import accepts a
multipart/form-data upload of a CSV file. It requires an admin or
owner key and is rate-limited to 5 requests/minute.
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/suppression-list/import \
-H "Authorization: Bearer $ORBIT_API_KEY" \
-F "file=@suppressions.csv" \
-F "default_country=US" \
-F "default_reason=migrated_from_legacy_platform" \
-F "dry_run=false"
| Field | Type | Notes |
|---|
file | file | Required. A single CSV, ≤ 25 MB, ≤ 100,000 rows. |
default_country | string | ISO-3166-1 alpha-2. Used to normalise national-format phone numbers to E.164. |
default_reason | string | Applied to every accepted row (≤ 512 chars). |
dry_run | boolean | When true, parse and classify only — no database writes. Use it to preview a file before committing. |
The first row is a header. Column names are case-insensitive and
position-independent, and common aliases are accepted:
| Logical column | Accepted headers |
|---|
| Phone | phone, phonenumber, mobile, msisdn |
| Email | email, emailaddress, mail |
| WhatsApp ID | wa_id, whatsapp, whatsappid |
| Reason (optional) | reason, note, notes |
| Channel (optional override) | channel |
Each row must contain at least one of phone / email / wa_id. A
single row may carry several address types — each produces its own
suppression entry. Example:
phone,email,reason
+14155550101,,replied STOP
,jordan@example.com,unsubscribed via email
+442071838750,sam@example.co.uk,complaint
If a channel column is present it overrides the default scope for
that row and must be one of the scope values listed above.
Per-row results
The response reports outcomes per row. Accepted rows are written;
others are classified, never silently dropped.
{
"data": {
"run_id": "supimp_4d…",
"total_rows": 1000,
"accepted": 950,
"duplicates": 30,
"intra_file_duplicates": 15,
"invalid": 5,
"errors": [
{ "status": "invalid", "reason": "invalid_phone", "raw_line": 42 }
],
"by_channel": { "all": 800, "email": 150 },
"file_sha256": "9b2e…"
},
"meta": { "request_id": "…", "timestamp": "2026-06-08T12:00:00.000Z" }
}
| Counter | Meaning |
|---|
accepted | Rows newly written to the suppression list. |
intra_file_duplicates | Rows that repeat an earlier (channel, address) within this same file. |
duplicates | Rows already suppressed from a prior import (skipped, no-op). |
invalid | Rows that failed validation — see errors[]. |
by_channel | Accepted counts grouped by channel scope. |
file_sha256 | Content hash of the upload, recorded for audit. |
The two duplicate counters are reported separately and on
purpose: intra_file_duplicates are repeats inside the file you
just uploaded, while duplicates were already on your list from
earlier. Neither is an error, and neither is silently swallowed —
both are counted so your reconciliation adds up.
Validation reasons
Each errors[] entry carries a friendly reason and the source
raw_line so you can fix and re-upload:
reason | Cause |
|---|
missing_address | The row had no phone, email, or wa_id. |
invalid_phone | Phone could not be normalised to E.164. |
invalid_email | Email failed RFC-5321 shape validation. |
invalid_wa_id | WhatsApp ID was not a valid E.164 number. |
row_too_long | A cell exceeded 4096 characters. |
too_many_columns | The row had more than 32 columns. |
Limits
| Limit | Value |
|---|
| Max file size | 25 MB |
| Max rows per request | 100,000 |
| Max cell length | 4,096 chars |
| Max columns per row | 32 |
| Max reason length | 512 chars |
| Server-side timeout | 60 s (a partial import returns 408 with the counts processed so far) |
For volumes beyond 100,000 rows, split the file and import in batches —
duplicate detection means re-importing overlapping ranges is safe.
Removing a suppression (re-opt-in)
To bring an address back, record a fresh opt-in through the
Consent API (opt_in: true). That
revokes the matching suppression entry and clears the STOP-fence.
Never re-message a previously suppressed contact without a documented,
fresh consent event.