> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Listes de désinscription et de suppression

> Comment Orbit supprime les destinataires désinscrits sur tous les canaux, comment importer en masse une liste de suppression depuis un CSV avec résultats par ligne et déduplication, et comment vérifier l'application, diagnostiquer les échecs d'import et lever une suppression en toute sécurité.

# Listes de désinscription et de suppression

Une **liste de suppression** est l'ensemble des adresses auxquelles vous ne
devez plus jamais envoyer de message — les personnes qui ont répondu STOP,
se sont désinscrites, ont généré un bounce ou se sont plaintes. Honorer
cette liste est une exigence légale sur chaque canal réglementé, et Orbit
la traite comme une porte d'envoi dure : une adresse supprimée est
abandonnée avant le dispatch, quel que soit la campagne, l'import de
contacts ou l'appel API.

Cette page explique comment fonctionne la suppression, comment **importer
en masse** une liste de suppression existante — par exemple lors d'une
migration depuis une autre plateforme — via un seul dépôt CSV, et comment
[exporter le registre](#export-the-suppression-list) pour un audit.

Tous les endpoints ci-dessous sont enracinés sur
`https://api.orbit.devotel.io/api/v1/compliance`.

***

## Comment se produit la suppression

Une adresse rejoint la liste de suppression de plusieurs façons :

* Un contact répond avec un **mot-clé STOP** sur SMS/WhatsApp.
* Un contact se désinscrit via le [centre de préférences](/compliance/send-gates#preference-center).
* Vous enregistrez une désinscription via
  [l'API de consentement](/compliance/consent-management) (`opt_in: false`).
* Vous **importez en masse** une liste (cette page).

Chaque entrée possède une **portée de canal**. L'ensemble complet des
portées est : `all`, `sms`, `voice`, `whatsapp`, `email`, `push`,
`telegram`, `messenger`, `rcs`.

La portée choisie dépend du point d'entrée :

* **L'import CSV en masse** déduit la portée du type d'adresse de chaque
  ligne : les adresses téléphoniques et WhatsApp prennent par défaut la
  portée `all` — un signal STOP sur un numéro de téléphone supprime tous
  les canaux joignables sur ce numéro — tandis que les adresses e-mail
  sont limitées à `email`. Une colonne `channel` remplace cette valeur
  par ligne (voir [Import CSV en masse](#bulk-csv-import)).
* **L'API de consentement et le centre de préférences** suppriment
  toujours avec la portée `all`, que l'identifiant enregistré soit un
  numéro de téléphone ou une adresse e-mail. Une désinscription via l'un
  de ces points d'entrée retire le contact de tous les canaux.

<Note>
  Quelle que soit la façon dont un numéro de téléphone est supprimé, les
  portes voix et composeur l'honorent : un numéro qui se désinscrit sur
  n'importe quel canal cesse de recevoir des appels aussi bien que des
  messages. Le mécanisme diffère selon le point d'entrée. Un **import CSV
  en masse** duplique en outre les lignes téléphoniques vers la liste DNC
  et marque les contacts correspondants. Une désinscription par **mot-clé
  STOP**, par le **centre de préférences** ou par l'**API de
  consentement** est au contraire enregistrée avec la portée `all`, que
  les portes voix et composeur lisent directement depuis la liste de
  suppression — l'appel reste bloqué, mais aucune ligne DNC ni marque de
  contact séparée n'est écrite.
</Note>

***

## Import CSV en masse

`POST /compliance/suppression-list/import` accepte un dépôt
`multipart/form-data` d'un fichier CSV. Il exige une clé admin ou owner
et est limité à 5 requêtes/minute.

```bash theme={null}
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"
```

### Champs du formulaire

| Field             | Type    | Notes                                                                                                                                 |
| ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `file`            | file    | **Obligatoire.** Un seul CSV, ≤ 25 Mo, ≤ 100 000 lignes.                                                                              |
| `default_country` | string  | ISO-3166-1 alpha-2. Sert à normaliser les numéros de téléphone au format national vers E.164.                                         |
| `default_reason`  | string  | Appliqué à chaque ligne acceptée (≤ 512 caractères).                                                                                  |
| `dry_run`         | boolean | Lorsque `true`, analyse et classifie seulement — aucune écriture en base. Utilisez-le pour prévisualiser un fichier avant de valider. |

### Format du CSV

La première ligne est un en-tête. Les noms de colonnes sont
**insensibles à la casse** et **indépendants de la position**, et des
alias courants sont acceptés :

| 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`                                  |

Chaque ligne doit contenir **au moins l'un** de phone / email / wa\_id.
Une même ligne peut porter plusieurs types d'adresses — chacun produit
sa propre entrée de suppression. Exemple :

```csv theme={null}
phone,email,reason
+14155550101,,replied STOP
,jordan@example.com,unsubscribed via email
+442071838750,sam@example.co.uk,complaint
```

Si une colonne `channel` est présente, elle remplace la portée par
défaut pour cette ligne et doit être l'une des valeurs de portée
listées ci-dessus.

### Résultats par ligne

La réponse rapporte les résultats par ligne. Les lignes acceptées sont
écrites ; les autres sont classifiées, jamais abandonnées silencieusement.

```json theme={null}
{
  "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`              | Lignes nouvellement écrites dans la liste de suppression.                     |
| `intra_file_duplicates` | Lignes qui répètent un `(channel, address)` déjà vu **dans ce même fichier**. |
| `duplicates`            | Lignes déjà supprimées par un import **antérieur** (ignorées, sans effet).    |
| `invalid`               | Lignes ayant échoué à la validation — voir `errors[]`.                        |
| `by_channel`            | Comptes acceptés groupés par portée de canal.                                 |
| `file_sha256`           | Empreinte du contenu du dépôt, enregistrée pour l'audit.                      |

<Note>
  Les deux compteurs de doublons sont rapportés **séparément et à
  dessein** : `intra_file_duplicates` sont des répétitions à l'intérieur
  du fichier que vous venez de déposer, tandis que `duplicates` étaient
  déjà sur votre liste auparavant. Aucun des deux n'est une erreur, et
  aucun n'est absorbé silencieusement — les deux sont comptés pour que
  votre rapprochement soit exact.
</Note>

### Raisons de validation

Chaque entrée `errors[]` porte une `reason` lisible et la `raw_line`
source pour que vous puissiez corriger et redéposer :

| `reason`           | Cause                                                |
| ------------------ | ---------------------------------------------------- |
| `missing_address`  | La ligne n'avait ni phone, ni email, ni wa\_id.      |
| `invalid_phone`    | Le téléphone n'a pas pu être normalisé en E.164.     |
| `invalid_email`    | L'e-mail a échoué à la validation de forme RFC-5321. |
| `invalid_wa_id`    | L'ID WhatsApp n'était pas un numéro E.164 valide.    |
| `row_too_long`     | Une cellule dépassait 4 096 caractères.              |
| `too_many_columns` | La ligne comportait plus de 32 colonnes.             |

### Limites

| Limit                                | Value                                                                       |
| ------------------------------------ | --------------------------------------------------------------------------- |
| Taille maximale du fichier           | 25 Mo                                                                       |
| Nombre maximal de lignes par requête | 100 000                                                                     |
| Longueur maximale d'une cellule      | 4 096 caractères                                                            |
| Nombre maximal de colonnes par ligne | 32                                                                          |
| Longueur maximale du motif           | 512 caractères                                                              |
| Délai côté serveur                   | 60 s (un import partiel renvoie `408` avec les comptes traités jusqu'alors) |

Au-delà de 100 000 lignes, découpez le fichier et importez par lots —
la détection des doublons rend sûre la ré-importation de plages qui se
chevauchent.

### En cas d'échec de l'import

Diagnostiquez les échecs à deux niveaux : **rejets au niveau HTTP**
(rien n'est écrit) et **classifications au niveau ligne** (le fichier
est accepté mais certaines lignes ne le sont pas).

Rejets au niveau HTTP :

| Status                       | Meaning                                                                                                                  | How to fix                                                                                                                       |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `400 NO_FILE`                | Aucune partie `file` dans le corps multipart.                                                                            | Joignez le CSV comme champ multipart `file`.                                                                                     |
| `400 MULTIPLE_FILES`         | Plus d'un fichier joint.                                                                                                 | Envoyez un seul CSV par requête.                                                                                                 |
| `400 CSV_PARSE_ERROR`        | Guillemets mal formés — un `"` non terminé ou une ligne d'en-tête manquante.                                             | Réexportez en CSV RFC-4180 (UTF-8) ; vérifiez que chaque guillemet a sa paire fermante.                                          |
| `400 MULTIPART_PARSE_FAILED` | Le corps n'était pas un multipart/form-data valide.                                                                      | Définissez `Content-Type: multipart/form-data` et ne pré-encodez pas le corps.                                                   |
| `408 IMPORT_TIMEOUT`         | L'exécution a dépassé le budget de 60 secondes. La réponse nomme les lignes déjà supprimées ; les autres ne le sont pas. | Découpez le reste en fichiers plus petits et relancez — les lignes supprimées avant le délai sont rapportées comme `duplicates`. |
| `413 PAYLOAD_TOO_LARGE`      | Le fichier dépasse 25 Mo.                                                                                                | Découpez en fichiers de moins de 25 Mo chacun.                                                                                   |
| `413 TOO_MANY_ROWS`          | Le fichier analysé dépasse 100 000 lignes.                                                                               | Découpez en fichiers de ≤ 100 000 lignes chacun.                                                                                 |
| `415 UNSUPPORTED_MEDIA_TYPE` | Le dépôt n'était pas un CSV (exporter un `.xlsx` est le déclencheur courant).                                            | Réexportez en CSV (UTF-8).                                                                                                       |
| `422 MISSING_ADDRESS_COLUMN` | La ligne d'en-tête n'a aucune colonne d'adresse reconnaissable.                                                          | Incluez au moins l'un de `phone`, `email`, `wa_id` comme en-tête (les alias sont listés sous [Format du CSV](#csv-format)).      |
| `422 VALIDATION_ERROR`       | Un champ de formulaire optionnel a échoué à la validation.                                                               | Vérifiez que `default_country` est un code ISO à 2 lettres et que `default_reason` fait ≤ 512 caractères.                        |
| `429`                        | Plus de 5 requêtes d'import en une minute.                                                                               | Attendez la fermeture de la fenêtre puis réessayez — mettez les lots en file plutôt que de marteler.                             |

Les classifications au niveau ligne (les entrées `errors[]` qui
accompagnent un import réussi) correspondent aux causes suivantes :

| `reason`           | Cause                                                                                                                   | How to fix                                                                                                            |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `missing_address`  | La ligne n'avait ni phone, ni email, ni wa\_id — ou sa substitution `channel` n'était pas l'une des portées autorisées. | Remplissez au moins une cellule d'adresse ; restreignez la colonne `channel` aux valeurs de portée listées ci-dessus. |
| `invalid_phone`    | Le téléphone n'a pas pu être normalisé en E.164.                                                                        | Corrigez le numéro, ou passez `default_country` pour que les numéros au format national soient analysés.              |
| `invalid_email`    | L'e-mail a échoué à la validation de forme RFC-5321 (ou dépassait 254 caractères).                                      | Corrigez l'adresse ; les espaces parasites et un `@` manquant sont les coupables habituels.                           |
| `invalid_wa_id`    | L'ID WhatsApp n'était pas un numéro E.164 valide.                                                                       | Utilisez le MSISDN du destinataire au format E.164 (le `+` de tête est optionnel).                                    |
| `row_too_long`     | Une cellule dépassait 4 096 caractères.                                                                                 | Raccourcissez la cellule — généralement un bloc collé a atterri dans la mauvaise colonne.                             |
| `too_many_columns` | La ligne comportait plus de 32 colonnes.                                                                                | Réexportez avec un seul délimiteur ; des virgules non guillemetées dans une cellule la scindent en colonnes fantômes. |

Seules les 100 premières entrées `errors[]` sont renvoyées avec le
détail complet — le compteur `invalid` reflète toujours le total réel.
L'assistant du tableau de bord (ci-dessous) empaquette les lignes
signalées dans un `skipped.csv` téléchargeable pour que vous puissiez
corriger et réimporter uniquement les échecs.

### Import depuis le tableau de bord

Le même endpoint est enveloppé par un assistant guidé dans **Settings →
Compliance → Opt-out lists → Import suppression list** — le même
contrat CSV, sans terminal.

1. **Choose CSV file** — choisissez un `.csv` de moins de 25 Mo.
   Utilisez **Download sample CSV** dans la boîte de dialogue pour un
   fichier de départ préformaté.
2. **Set defaults (optional)** — un `Default country` (ISO alpha-2)
   pour normaliser les numéros au format national, et un `Reason` en
   texte libre apposé sur chaque ligne acceptée.
3. **Preview** — exécute l'import en dry run côté serveur : rien n'est
   écrit, et la boîte de dialogue affiche la répartition acceptées /
   déjà listées / répétées dans le fichier / invalides avant que vous
   ne validiez.
4. **Confirm import** — effectue l'écriture définitive. Si des lignes
   étaient invalides, téléchargez **`skipped.csv`** pour les corriger
   et les réimporter.

L'assistant applique aussi les vérifications de type de fichier et de
25 Mo côté client, pour qu'un export mal nommé échoue avant même
d'atteindre l'API.

***

## Exporter la liste de suppression

`GET /compliance/suppression-list/export` télécharge le registre de
suppression — le pendant symétrique de l'[import](#bulk-csv-import)
ci-dessus. Utilisez-le pour prouver à un régulateur ou à un auditeur
quelles adresses étaient supprimées à un instant donné, y compris les
numéros importés en masse sans contact correspondant.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/compliance/suppression-list/export?format=csv&status=active" \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -o suppression-list.csv
```

Paramètres de requête :

| Parameter     | Type    | Notes                                                                                                |
| ------------- | ------- | ---------------------------------------------------------------------------------------------------- |
| `format`      | enum    | `csv` (par défaut) ou `json`.                                                                        |
| `channel`     | enum    | Restreindre à une portée de canal.                                                                   |
| `status`      | enum    | `active` (par défaut — l'ensemble que chaque porte d'envoi applique réellement), `revoked` ou `all`. |
| `from` / `to` | string  | Plage de dates sur `suppressed_at`. Une date nue `YYYY-MM-DD` ou un datetime RFC-3339.               |
| `limit`       | integer | Nombre de lignes à inclure (1–50 000, 50 000 par défaut).                                            |

Chaque ligne porte `suppression_id`, `channel`, `address`, le `status`
dérivé (`active` ou `revoked`), `reason`, `source`, `contact_id` (vide
pour les adresses importées en masse sans contact), `notes`, ainsi que
les horodatages `suppressed_at` / `revoked_at` / `created_at`.

L'accès est restreint aux clés **owner** et **admin**, et chaque
exécution d'export est inscrite au journal d'audit. Lorsque le registre
dépasse 50 000 lignes, la réponse CSV porte un en-tête
`X-Export-Truncated: true` (l'équivalent JSON définit `truncated:
true`) — filtrez par canal ou exportez des fenêtres de dates
consécutives pour capturer la fin.

***

## Vérifier qu'une suppression a pris effet

Faire confiance mais vérifier : après un import (ou toute
désinscription), confirmez que la porte d'envoi bloque effectivement
l'adresse avant de confier la liste à une campagne.

1. **Envoyez un message de test à l'adresse supprimée.** Un envoi API
   direct vers un destinataire supprimé échoue de façon synchrone avec
   HTTP 422 et le code d'erreur `RECIPIENT_OPTED_OUT`. En [mode
   sandbox](/sandbox/magic-numbers), aucun opérateur n'est touché et
   aucun solde n'est débité ; tout destinataire se terminant par `8`
   se résout aussi vers l'accusé de livraison simulé `blocked`, qui est
   la vue côté opérateur de la même barrière.

   ```bash theme={null}
   curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
     -H "X-API-Key: $ORBIT_SANDBOX_KEY" \
     -H "Content-Type: application/json" \
     -d '{"to": "+14155550101", "from": "+15005550101", "body": "gate check"}'
   ```

   ```json theme={null}
   {
     "error": {
       "code": "RECIPIENT_OPTED_OUT",
       "message": "Recipient has opted out of this channel"
     }
   }
   ```

   Un envoi de campagne vers la même adresse se comporte différemment
   par conception : le destinataire est ignoré silencieusement
   (`status: "skipped"`, `reason: "opted_out"`) pour que le lot
   continue — consultez le rapport par destinataire de la campagne
   plutôt que d'attendre une erreur.
2. **Confirmez que l'entrée figure dans le registre.** Exportez avec
   [la requête ci-dessus](#export-the-suppression-list) (`status=active`
   est la valeur par défaut) et vérifiez que l'adresse apparaît avec la
   portée `channel` attendue. L'export est la source de vérité que
   chaque porte d'envoi lit — si la ligne y est `active`, la barrière
   est levée.

Les deux vérifications répondent à des questions différentes : l'étape
1 prouve l'application (la porte se déclenche), l'étape 2 prouve la
portée (l'entrée existe avec le canal voulu).

***

## Levée d'une suppression (ré-opt-in)

Pour rétablir une adresse, enregistrez un nouvel opt-in via
[l'API de consentement](/compliance/consent-management)
(`opt_in: true`). Cela révoque l'entrée de suppression correspondante
et lève la barrière STOP. Ne re-messagez jamais un contact précédemment
supprimé sans un événement de consentement nouveau et documenté.

**Confirmation que la barrière est levée.** Exportez avec
`status=revoked` et trouvez l'adresse : la ligne reste présente pour
l'audit avec `status: revoked` et un horodatage `revoked_at` —
l'historique de suppression n'est jamais supprimé, seulement révoqué.
Envoyez ensuite un petit message de test à l'adresse comme dans
[Vérifier qu'une suppression a pris
effet](#verify-a-suppression-took-effect) : une soumission réussie (pas
de `RECIPIENT_OPTED_OUT`) confirme que la porte ne se déclenche plus
sur l'historique révoqué. Tant que les deux vérifications ne passent
pas, traitez l'adresse comme encore bloquée.

***

## Références associées

* [Consent Management](/compliance/consent-management) — enregistrer et
  consulter le consentement par canal.
* [Send Gates](/compliance/send-gates) — heures de silence, DNC, RND,
  RMD, arrêt d'urgence et centre de préférences.
* [DSAR](/compliance/dsar) — comment les requêtes `delete` / `opt_out`
  aboutissent à la suppression.
* [API Reference → Opt-outs](/api-reference/optouts) — schémas des
  endpoints de désinscription et de suppression.
