Skip to main content

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"

Form fields

FieldTypeNotes
filefileRequired. A single CSV, ≤ 25 MB, ≤ 100,000 rows.
default_countrystringISO-3166-1 alpha-2. Used to normalise national-format phone numbers to E.164.
default_reasonstringApplied to every accepted row (≤ 512 chars).
dry_runbooleanWhen true, parse and classify only — no database writes. Use it to preview a file before committing.

CSV format

The first row is a header. Column names are case-insensitive and position-independent, and common aliases are accepted:
Logical columnAccepted headers
Phonephone, phonenumber, mobile, msisdn
Emailemail, emailaddress, mail
WhatsApp IDwa_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" }
}
CounterMeaning
acceptedRows newly written to the suppression list.
intra_file_duplicatesRows that repeat an earlier (channel, address) within this same file.
duplicatesRows already suppressed from a prior import (skipped, no-op).
invalidRows that failed validation — see errors[].
by_channelAccepted counts grouped by channel scope.
file_sha256Content 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:
reasonCause
missing_addressThe row had no phone, email, or wa_id.
invalid_phonePhone could not be normalised to E.164.
invalid_emailEmail failed RFC-5321 shape validation.
invalid_wa_idWhatsApp ID was not a valid E.164 number.
row_too_longA cell exceeded 4096 characters.
too_many_columnsThe row had more than 32 columns.

Limits

LimitValue
Max file size25 MB
Max rows per request100,000
Max cell length4,096 chars
Max columns per row32
Max reason length512 chars
Server-side timeout60 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.