Skip to main content

Bulk-import a suppression list from CSV

Use the bulk import when you are bringing a suppression ledger from another provider, applying a batch from a CRM, or remediating a known exposure. The endpoint writes each accepted address to your tenant’s suppression ledger and returns enough detail to reconcile the file without guessing which rows were new. This is an operator-side control. Devotel Orbit does not decide which recipients your tenant must suppress or replace your legal process. Review the three suppression entry points when you need to choose between a CSV backfill, a live Consent API event, or a recipient-managed Preference Center.
A phone or WhatsApp row with no channel value becomes channel: all. That scope blocks the address on every supported send path. Do not add a channel override unless your source ledger explicitly represents a channel-only restriction.

Before you import

Prepare a UTF-8 RFC-4180 CSV with a header and confirm that your source system has already identified the correct country for any national-format phone numbers. The endpoint accepts one multipart file per request and requires an owner or admin credential. The operation is POST /api/v1/compliance/suppression-list/import.
default_country is optional. Use it when the CSV contains national-format phone numbers such as 4155550101; it is a two-letter ISO 3166-1 alpha-2 country hint. You, as the operator, are responsible for supplying the right country. Orbit cannot safely infer whether an unqualified number belongs to one country or another. A number that cannot be normalized is returned as invalid_phone instead of being written. Use dry_run=true with the same file and defaults to parse and classify rows without writing them. A dry run returns a preview, but it does not create an audit-chain import record because no suppression write occurred.

CSV contract

The first row is the header. Parsing follows RFC 4180: quote a field that contains a comma or newline, escape a literal quote by doubling it, and use CRLF or LF row endings. A UTF-8 BOM is accepted and removed from the first header. Header matching is case-insensitive, ignores spaces, hyphens, and underscores, and accepts the aliases below. Header order does not matter. A row may contain more than one address. Each populated address becomes its own suppression entry, with the row’s channel override applied to each one. For example, this file creates one all phone entry and one email entry on the first data row:
An explicit channel must be one of all, sms, voice, whatsapp, email, push, telegram, messenger, or rcs. An invalid override is classified as a row-level invalid result; it does not make the entire file fail.

Address normalization

  • Phone: Orbit calls its phone normalizer. Valid input is stored in the canonical E.164 representation. Pass default_country for national-format input, and verify the resulting country before importing. Do not prepend a country code based on a guess in a batch script.
  • WhatsApp ID: Supply the recipient’s numeric MSISDN, with an optional leading +. Orbit validates the 7-to-15-digit E.164 range and stores the canonical + form. A bare national number is not made unambiguous by the wa_id column; convert it before upload.
  • Email: Orbit lowercases the address and requires a practical RFC 5321 mailbox shape. Addresses longer than 254 characters are invalid.
Orbit does not modify your source file. Keep the original file and the returned file_sha256 together so you can prove exactly which bytes were submitted.

Understand channel scope before committing

The suppression key is (channel, address). The default is intentionally based on the address type, not on the transport that delivered the original opt-out: The override is a deliberate narrowing or selection, not a declaration of where the CSV came from. A phone row with channel=email can be stored, but it will only match email sends. If a recipient still receives a message after an accepted import, first inspect the row’s scope and follow Troubleshooting: suppression scope mismatch. A scope mismatch is different from a failed import. Export the active ledger, check the channel value, and compare it with the send channel. Re-uploading the same (channel, address) is idempotent; it does not replace an existing row with a different scope. If you intentionally need both scopes, import the address with the additional key, and if you need to correct an existing scope, follow the revocation or correction procedure in the troubleshooting guide rather than assuming a duplicate changed it.

Walk through an import response

The successful response is an envelope whose data contains the run summary. The endpoint classifies bad rows and continues with the valid ones:
accepted counts newly inserted suppression keys. duplicates counts keys that were already active before this request. intra_file_duplicates counts repeated keys that were collapsed before the database write. invalid is the complete invalid count; only the first 100 invalid rows are expanded in errors. by_channel counts newly accepted rows by their stored scope.

Invalid and empty rows

A row with no value in any recognized address column returns a row-level classification, not an HTTP failure:
The same missing_address reason is used when a populated row has an invalid channel override. Fix the source row and re-upload it. If the header has no recognized address column at all, the request instead returns HTTP 422 with error code MISSING_ADDRESS_COLUMN; add phone, email, or wa_id (or one of the aliases in the schema table). Other row-level reasons include invalid_phone, invalid_email, invalid_wa_id, row_too_long, and too_many_columns.

Re-run a strict subset safely

The database’s active partial unique key makes a re-run safe. A second upload of a key that is already active is a no-op, and the response exposes it as a duplicates count. This lets you retry a timed-out or overlapping batch without creating a second active suppression row. File 1: the original ledger
Suppose the response is:
File 2: a strict subset used for a safe retry
The second response reports the two existing keys as duplicates:
The changed reason does not rewrite an active row. duplicates means “it was already active before this run,” while intra_file_duplicates means “the same key appeared more than once in this file.” Keep the second run’s run_id and hash as evidence of the retry even though it added no rows.

Capture audit-chain evidence

Every committing import writes one tamper-evident audit-chain row with the operation compliance.suppression_list_import. The row is associated with the tenant and contains: For a regulator or discovery request, preserve the original CSV, the response, and the audit export containing this run. Match the audit row’s hash to the file, match its counts to the response, and export the active suppression ledger for the affected time window. The ledger proves which entries were active; the audit row proves who ran the import and what the operator submitted. Do not send a raw CSV as the only evidence: it does not establish that the file was actually imported.

Post-incident remediation example

If a GDPR or CCPA review finds that a segment was exposed to an unintended campaign, use a repeatable tenant-owned sequence:
  1. Discover the exposure. Freeze the affected segment in your own incident process, identify the recipient identifiers and the communication window, and preserve the source report.
  2. Build the CSV. Deduplicate by intended (channel, address) scope. Use E.164 phone values, canonical WhatsApp IDs, and lowercase email values. Leave phone and wa_id rows untagged when the incident requires a cross-channel fence; add channel only for a documented channel-specific restriction.
  3. Preview and import. Run the exact file with dry_run=true, fix every invalid row, then commit the clean file with a reason such as post_incident_remediation_2026_10_11.
  4. Capture evidence. Save the run_id, file_sha256, response counts, and the audit-chain export. Export the active ledger and verify representative rows have the intended scope.
  5. Close the loop. Attach the evidence bundle to the tenant’s incident record and update the outbound audience process that admitted the exposed recipients. A suppression import is the containment step, not a substitute for the tenant’s privacy, consent, or incident-response work.
The Consent vs. suppression model explains why an active suppression fence wins over an affirmative consent record. For revocation signals and reasonable channel interpretation, see TCPA reasonable revocation.

Limits and HTTP failures

Split larger ledgers into batches. A batch that hits the 60-second budget may have already written rows; its 408 IMPORT_TIMEOUT response identifies the partial counts, and a retry is safe because active keys are idempotent. These request-level failures write no normal completion summary:
  • 400 NO_FILE or MULTIPLE_FILES: attach exactly one multipart file.
  • 400 CSV_PARSE_ERROR: fix malformed quoting or the missing header.
  • 413 PAYLOAD_TOO_LARGE or TOO_MANY_ROWS: split the source file.
  • 415 UNSUPPORTED_MEDIA_TYPE: export CSV rather than XLSX.
  • 422 MISSING_ADDRESS_COLUMN: add a recognized address header.
  • 422 VALIDATION_ERROR: correct default_country or the form-level default_reason.
A malformed address inside an otherwise valid file is not one of these request-level failures. It is returned in errors[], counted in invalid, and leaves the other rows eligible for import.

How this differs from other opt-out paths

All of these inputs converge on the tenant’s suppression ledger, but they serve different operational jobs. Keep one row per channel of truth in your source systems and avoid treating one input as a replacement for the others. Use the suppression three-entry-points guide for the selection decision. Use the Consent vs. suppression model and TCPA reasonable revocation to align your tenant’s consent and revocation records with the suppression fence.