> ## 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.

# Opt-out y listas de supresión

> Cómo suprime Orbit a los destinatarios que se han dado de baja en todos los canales, cómo importar masivamente una lista de supresión desde un CSV con resultados por fila y deduplicación, y cómo verificar su aplicación, diagnosticar fallos de importación y revertir una supresión de forma segura.

# Opt-out y listas de supresión

Una **lista de supresión** es el conjunto de direcciones a las que
nunca debe volver a enviar mensajes: personas que respondieron STOP, se
dieron de baja, generaron rebotes o presentaron reclamaciones.
Respetarla es un requisito legal en todos los canales regulados, y
Orbit la trata como una puerta de envío estricta: una dirección
suprimida se descarta antes del envío, independientemente de la
campaña, la importación de contactos o la llamada a la API.

Esta página explica cómo funciona la supresión, cómo **importar
masivamente** una lista de supresión existente — por ejemplo, al migrar
desde otra plataforma — mediante una única carga de CSV, y cómo
[exportar el registro](#exportar-la-lista-de-supresión) para una
auditoría.

Todos los endpoints siguientes están enraizados en
`https://api.orbit.devotel.io/api/v1/compliance`.

***

## Cómo se produce la supresión

Una dirección llega a la lista de supresión de varias formas:

* Un contacto responde con una **palabra clave STOP** en SMS/WhatsApp.
* Un contacto se da de baja a través del [Centro de preferencias](/compliance/send-gates#preference-center).
* Usted registra una baja a través de la
  [API de consentimiento](/compliance/consent-management) (`opt_in: false`).
* Usted **importa masivamente** una lista (esta página).

Cada entrada tiene un **ámbito de canal**. El conjunto completo de
ámbitos es: `all`, `sms`, `voice`, `whatsapp`, `email`, `push`,
`telegram`, `messenger`, `rcs`.

El ámbito elegido depende del punto de entrada:

* La **importación masiva de CSV** infiere el ámbito a partir del tipo
  de dirección de cada fila: las direcciones de teléfono y de WhatsApp
  tienen por defecto el ámbito `all` — una señal de STOP en un número
  de teléfono suprime todos los canales alcanzables en ese número —
  mientras que las direcciones de correo electrónico se limitan al
  ámbito `email`. Una columna `channel` sobrescribe esto por fila
  (consulte [Importación masiva de CSV](#importación-masiva-de-csv)).
* **La API de consentimiento y el Centro de preferencias** siempre
  suprimen con el ámbito `all`, independientemente de si el
  identificador registrado es un número de teléfono o una dirección de
  correo electrónico. Una baja a través de cualquiera de estos puntos
  de entrada elimina al contacto de todos los canales.

<Note>
  Sea cual sea la forma en que se suprima un número de teléfono, las
  puertas de voz y del marcador lo respetan: un número que se da de
  baja en cualquier canal deja de recibir tanto llamadas como mensajes.
  El mecanismo difiere según el punto de entrada. Una **importación
  masiva de CSV** además refleja las filas de teléfono en la lista DNC
  y marca los contactos coincidentes. Una baja por **palabra clave
  STOP**, por el **Centro de preferencias** o por la **API de
  consentimiento** se registra en su lugar con el ámbito `all`, que las
  puertas de voz y del marcador leen directamente de la lista de
  supresión: la llamada sigue bloqueada, pero no se escribe una fila
  separada en la lista DNC ni una marca en el contacto.
</Note>

***

## Importación masiva de CSV

`POST /compliance/suppression-list/import` acepta una carga
`multipart/form-data` de un archivo CSV. Requiere una clave de
administrador o de propietario y está limitada a 5 solicitudes por
minuto.

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

### Campos del formulario

| Campo             | Tipo     | Notas                                                                                                                                    |
| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `file`            | archivo  | **Obligatorio.** Un único CSV, ≤ 25 MB, ≤ 100.000 filas.                                                                                 |
| `default_country` | cadena   | ISO-3166-1 alpha-2. Se usa para normalizar números de teléfono en formato nacional a E.164.                                              |
| `default_reason`  | cadena   | Se aplica a todas las filas aceptadas (≤ 512 caracteres).                                                                                |
| `dry_run`         | booleano | Si es `true`, solo analiza y clasifica: no hay escrituras en la base de datos. Úselo para previsualizar un archivo antes de confirmarlo. |

### Formato del CSV

La primera fila es el encabezado. Los nombres de columna son
**insensibles a mayúsculas** e **independientes de la posición**, y se
aceptan alias comunes:

| Columna lógica                 | Encabezados aceptados                      |
| ------------------------------ | ------------------------------------------ |
| Teléfono                       | `phone`, `phonenumber`, `mobile`, `msisdn` |
| Correo electrónico             | `email`, `emailaddress`, `mail`            |
| ID de WhatsApp                 | `wa_id`, `whatsapp`, `whatsappid`          |
| Motivo (opcional)              | `reason`, `note`, `notes`                  |
| Canal (sobrescritura opcional) | `channel`                                  |

Cada fila debe contener **al menos una** de las columnas phone / email
/ wa\_id. Una misma fila puede llevar varios tipos de dirección: cada
una genera su propia entrada de supresión. Ejemplo:

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

Si hay una columna `channel`, sobrescribe el ámbito por defecto de esa
fila y debe ser uno de los valores de ámbito enumerados arriba.

### Resultados por fila

La respuesta informa de resultados por fila. Las filas aceptadas se
escriben; las demás se clasifican, nunca se descartan silenciosamente.

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

| Contador                | Significado                                                                          |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `accepted`              | Filas escritas por primera vez en la lista de supresión.                             |
| `intra_file_duplicates` | Filas que repiten un `(channel, address)` anterior **dentro de este mismo archivo**. |
| `duplicates`            | Filas ya suprimidas por una importación **anterior** (se omiten, sin efecto).        |
| `invalid`               | Filas que no pasaron la validación: consulte `errors[]`.                             |
| `by_channel`            | Recuentos de aceptadas agrupados por ámbito de canal.                                |
| `file_sha256`           | Hash de contenido de la carga, registrado para auditoría.                            |

<Note>
  Los dos contadores de duplicados se informan **por separado y a
  propósito**: `intra_file_duplicates` son repeticiones dentro del
  archivo que acaba de subir, mientras que `duplicates` ya estaban en
  su lista de antes. Ninguno es un error y ninguno se traga
  silenciosamente: ambos se cuentan para que su reconciliación cuadre.
</Note>

### Motivos de validación

Cada entrada de `errors[]` lleva un `reason` descriptivo y la línea de
origen `raw_line` para que pueda corregir y volver a subir:

| `reason`           | Causa                                                          |
| ------------------ | -------------------------------------------------------------- |
| `missing_address`  | La fila no tenía phone, email ni wa\_id.                       |
| `invalid_phone`    | El teléfono no pudo normalizarse a E.164.                      |
| `invalid_email`    | El correo electrónico no pasó la validación de forma RFC-5321. |
| `invalid_wa_id`    | El ID de WhatsApp no era un número E.164 válido.               |
| `row_too_long`     | Una celda superó los 4.096 caracteres.                         |
| `too_many_columns` | La fila tenía más de 32 columnas.                              |

### Límites

| Límite                        | Valor                                                                                       |
| ----------------------------- | ------------------------------------------------------------------------------------------- |
| Tamaño máximo de archivo      | 25 MB                                                                                       |
| Máximo de filas por solicitud | 100.000                                                                                     |
| Longitud máxima de celda      | 4.096 caracteres                                                                            |
| Máximo de columnas por fila   | 32                                                                                          |
| Longitud máxima del motivo    | 512 caracteres                                                                              |
| Tiempo de espera del servidor | 60 s (una importación parcial devuelve `408` con los recuentos procesados hasta el momento) |

Para volúmenes superiores a 100.000 filas, divida el archivo e importe
por lotes: la detección de duplicados hace que reimportar rangos que se
solapen sea seguro.

### Cuando falla una importación

Diagnostique los fallos en dos niveles: **rechazos a nivel HTTP** (no
se escribe nada) y **clasificaciones a nivel de fila** (el archivo se
acepta, pero filas concretas no).

Rechazos a nivel HTTP:

| Estado                       | Significado                                                                                                   | Cómo solucionarlo                                                                                                                            |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 NO_FILE`                | No hay parte `file` en el cuerpo multipart.                                                                   | Adjunte el CSV como campo multipart `file`.                                                                                                  |
| `400 MULTIPLE_FILES`         | Se adjuntó más de un archivo.                                                                                 | Envíe un CSV por solicitud.                                                                                                                  |
| `400 CSV_PARSE_ERROR`        | Entrecomillado mal formado: un `"` sin cerrar o falta la fila de encabezado.                                  | Vuelva a exportar como CSV RFC-4180 (UTF-8); compruebe que cada comilla tenga su pareja de cierre.                                           |
| `400 MULTIPART_PARSE_FAILED` | El cuerpo no era multipart/form-data válido.                                                                  | Establezca `Content-Type: multipart/form-data` y no precodifique el cuerpo.                                                                  |
| `408 IMPORT_TIMEOUT`         | La ejecución superó el presupuesto de 60 segundos. La respuesta nombra las filas ya suprimidas; las demás no. | Divida el resto en archivos más pequeños y vuelva a ejecutar: las filas suprimidas antes del tiempo de espera se informan como `duplicates`. |
| `413 PAYLOAD_TOO_LARGE`      | El archivo supera los 25 MB.                                                                                  | Divida en archivos de menos de 25 MB cada uno.                                                                                               |
| `413 TOO_MANY_ROWS`          | El archivo analizado supera las 100.000 filas.                                                                | Divida en archivos de ≤ 100.000 filas cada uno.                                                                                              |
| `415 UNSUPPORTED_MEDIA_TYPE` | La carga no era un CSV (exportar `.xlsx` es la causa habitual).                                               | Vuelva a exportar como CSV (UTF-8).                                                                                                          |
| `422 MISSING_ADDRESS_COLUMN` | La fila de encabezado no tiene ninguna columna de dirección reconocible.                                      | Incluya al menos uno de `phone`, `email`, `wa_id` como encabezado (los alias se enumeran en [Formato del CSV](#formato-del-csv)).            |
| `422 VALIDATION_ERROR`       | Un campo de formulario opcional no pasó la validación.                                                        | Compruebe que `default_country` sea un código ISO de 2 letras y `default_reason` tenga ≤ 512 caracteres.                                     |
| `429`                        | Más de 5 solicitudes de importación en un minuto.                                                             | Espere a que se cierre la ventana y vuelva a intentarlo: encole los lotes masivos en lugar de insistir.                                      |

Las clasificaciones a nivel de fila (las entradas de `errors[]` que
acompañan a una importación correcta) corresponden a causas como
sigue:

| `reason`           | Causa                                                                                                          | Cómo solucionarlo                                                                                                            |
| ------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `missing_address`  | La fila no tenía phone, email ni wa\_id, o su sobrescritura de `channel` no era uno de los ámbitos permitidos. | Rellene al menos una celda de dirección; restrinja la columna `channel` a los valores de ámbito enumerados arriba.           |
| `invalid_phone`    | El teléfono no pudo normalizarse a E.164.                                                                      | Corrija el número o pase `default_country` para que los números en formato nacional se analicen.                             |
| `invalid_email`    | El correo electrónico no pasó la validación de forma RFC-5321 (o superó los 254 caracteres).                   | Corrija la dirección; los culpables habituales son espacios en blanco sobrantes y un `@` ausente.                            |
| `invalid_wa_id`    | El ID de WhatsApp no era un número E.164 válido.                                                               | Use el MSISDN del destinatario en formato E.164 (el `+` inicial es opcional).                                                |
| `row_too_long`     | Una celda superó los 4.096 caracteres.                                                                         | Acorte la celda: suele ser un bloque pegado que cayó en la columna equivocada.                                               |
| `too_many_columns` | La fila tenía más de 32 columnas.                                                                              | Vuelva a exportar con un único delimitador; las comas sin entrecomillar dentro de una celda la dividen en columnas fantasma. |

Solo se devuelven las primeras 100 entradas de `errors[]` con detalle
completo: el contador `invalid` siempre refleja el total real. El
asistente del panel (más abajo) empaqueta las filas detectadas como un
`skipped.csv` descargable para que pueda corregir y reimportar solo los
fallos.

### Importar desde el panel

El mismo endpoint está envuelto por un asistente guiado en **Settings →
Compliance → Opt-out lists → Import suppression list**: el mismo
contrato de CSV, sin necesidad de terminal.

1. **Elegir archivo CSV**: elija un `.csv` de menos de 25 MB. Use
   **Download sample CSV** en el diálogo para obtener un archivo
   inicial preformateado.
2. **Establecer valores por defecto (opcional)**: un `Default country`
   (ISO alpha-2) para normalizar números en formato nacional y un
   `Reason` de texto libre estampado en cada fila aceptada.
3. **Previsualizar**: ejecuta la importación como una ejecución de
   prueba en el servidor: no se escribe nada y el diálogo muestra el
   desglose de aceptadas / ya en la lista / repetidas en el archivo /
   inválidas antes de que confirme.
4. **Confirmar importación**: realiza la escritura que confirma. Si
   alguna fila fue inválida, descargue **`skipped.csv`** para
   corregirlas y reimportarlas.

El asistente también aplica las comprobaciones de tipo de archivo y de
25 MB en el cliente, de modo que una exportación mal hecha falla antes
de llegar a la API.

***

## Exportar la lista de supresión

`GET /compliance/suppression-list/export` descarga el registro de
supresión: la contraparte simétrica de la
[importación](#importación-masiva-de-csv) de arriba. Úselo para
demostrar a un regulador o auditor qué direcciones estaban suprimidas
en un momento dado, incluidos los números importados masivamente sin
contacto coincidente.

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

Parámetros de consulta:

| Parámetro     | Tipo        | Notas                                                                                             |
| ------------- | ----------- | ------------------------------------------------------------------------------------------------- |
| `format`      | enumeración | `csv` (por defecto) o `json`.                                                                     |
| `channel`     | enumeración | Restringe a un ámbito de canal.                                                                   |
| `status`      | enumeración | `active` (por defecto: el conjunto que cada puerta de envío realmente aplica), `revoked` o `all`. |
| `from` / `to` | cadena      | Rango de fechas sobre `suppressed_at`. Una fecha suelta `YYYY-MM-DD` o un datetime RFC-3339.      |
| `limit`       | entero      | Filas a incluir (1–50.000, por defecto 50.000).                                                   |

Cada fila lleva `suppression_id`, `channel`, `address`, el `status`
derivado (`active` o `revoked`), `reason`, `source`, `contact_id`
(vacío para direcciones importadas masivamente sin contacto), `notes` y
las marcas de tiempo `suppressed_at` / `revoked_at` / `created_at`.

El acceso está restringido a las claves de **propietario** y de
**administrador**, y cada exportación se escribe en el registro de
auditoría. Cuando el registro supera las 50.000 filas, la respuesta CSV
lleva un encabezado `X-Export-Truncated: true` (el equivalente JSON
establece `truncated: true`): filtre por canal o exporte ventanas de
fechas consecutivas para capturar el resto.

***

## Verificar que una supresión surtió efecto

Confíe pero verifique: tras una importación (o cualquier evento de
baja), confirme que la puerta de envío realmente bloquea la dirección
antes de entregar la lista a una campaña.

1. **Envíe un mensaje de prueba a la dirección suprimida.** Un envío
   directo por API a un destinatario suprimido falla de forma síncrona
   con HTTP 422 y el código de error `RECIPIENT_OPTED_OUT`. En [modo
   sandbox](/sandbox/magic-numbers) no se toca ningún operador y no se
   deduce saldo; cualquier destinatario que termine en `8` además se
   resuelve al recibo de entrega simulado `blocked`, que es la visión
   del operador de la misma barrera.

   ```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 envío de campaña a la misma dirección se comporta de forma
   diferente por diseño: el destinatario se omite silenciosamente
   (`status: "skipped"`, `reason: "opted_out"`) para que el lote siga
   avanzando: consulte el informe por destinatario de la campaña en
   lugar de esperar un error.
2. **Confirme que la entrada está en el registro.** Exporte con
   [la consulta anterior](#exportar-la-lista-de-supresión)
   (`status=active` es el valor por defecto) y compruebe que la
   dirección aparece con el ámbito `channel` esperado. La exportación
   es la fuente de verdad que lee cada puerta de envío: si la fila está
   `active` ahí, la barrera está levantada.

Las dos comprobaciones responden a preguntas distintas: el paso 1
demuestra la aplicación (la puerta salta), el paso 2 demuestra el
ámbito (la entrada existe con el canal que usted pretendía).

***

## Eliminar una supresión (nueva suscripción)

Para recuperar una dirección, registre una nueva suscripción a través
de la [API de consentimiento](/compliance/consent-management)
(`opt_in: true`). Eso revoca la entrada de supresión coincidente y
retira la barrera del STOP. Nunca vuelva a enviar mensajes a un
contacto previamente suprimido sin un evento de consentimiento fresco y
documentado.

**Confirmar que la barrera está bajada.** Exporte con `status=revoked`
y localice la dirección: la fila sigue ahí para auditoría con
`status: revoked` y una marca de tiempo `revoked_at`: el historial de
supresión nunca se elimina, solo se revoca. A continuación, envíe un
pequeño mensaje de prueba a la dirección como en [Verificar que una
supresión surtió efecto](#verificar-que-una-supresión-surtió-efecto):
un envío correcto (sin `RECIPIENT_OPTED_OUT`) confirma que la puerta ya
no salta sobre el historial revocado. Hasta que ambas comprobaciones
pasen, trate la dirección como todavía cercada.

***

## Referencias relacionadas

* [Gestión del consentimiento](/compliance/consent-management): registre
  y consulte el consentimiento por canal.
* [Puertas de envío](/compliance/send-gates): horas de silencio, DNC,
  RND, RMD, parada de emergencia y el centro de preferencias.
* [DSAR](/compliance/dsar): cómo las solicitudes `delete` / `opt_out`
  llegan a la supresión.
* [Referencia de API → Opt-outs](/api-reference/optouts): esquemas de
  los endpoints de baja y supresión.
