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

# Gestión del consentimiento y recibos

> Registre, consulte y audite el consentimiento de mensajería por canal en Orbit — incluido el seguimiento de la base jurídica del GDPR y los recibos firmados del Consent Manager de la DPDP de la India.

# Gestión del consentimiento y recibos

Antes de enviar mensajes a un contacto en un canal regulado, por lo
general necesita una base jurídica — la mayoría de las veces el
**consentimiento**. La API de consentimiento de Orbit es el sistema
de registro de quién aceptó o rechazó el consentimiento, en qué
canal, cuándo y bajo qué base jurídica. Cada escritura se propaga a
las superficies sobre las que se controlan sus envíos, por lo que
registrar el consentimiento aquí es lo que realmente desbloquea (o
bloquea) un mensaje. También puede
[exportar el registro completo](#exporting-the-consent-proof-of-record)
como un archivo CSV o JSON listo para auditorías.

Todos los endpoints que figuran a continuación tienen como raíz
`https://api.orbit.devotel.io/api/v1/compliance`.

<Warning>
  Registrar el consentimiento en Orbit crea un registro auditable,
  pero por sí solo no hace que un envío sea lícito. Usted sigue
  siendo responsable de obtener un consentimiento válido y del
  contenido que envía. Esta página no constituye asesoramiento
  jurídico.
</Warning>

***

## Canales y estados

El consentimiento se rastrea **por canal**. El conjunto de canales
admitidos es:

`email`, `fax`, `instagram`, `line`, `messenger`, `push`, `rcs`,
`sms`, `viber`, `voice`, `whatsapp`.

Un par `(contacto, canal)` se resuelve en uno de tres estados:

| Estado      | Significado                                                                                           |
| ----------- | ----------------------------------------------------------------------------------------------------- |
| `opted_in`  | Consentimiento otorgado y no revocado.                                                                |
| `opted_out` | Consentimiento revocado, o un opt-out explícito registrado.                                           |
| `unknown`   | No existe registro de consentimiento para el par — su puerta de envío decide el valor predeterminado. |

***

## Registro del consentimiento

`POST /compliance/consent` registra un opt-in o un opt-out en uno o
más canales en una sola llamada. Identifique el contacto mediante
`contact_id` **o** por `identifier` (un correo electrónico, un
teléfono E.164 o un ID de WhatsApp — Orbit resuelve el tipo
automáticamente).

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/consent \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "identifier": "jordan@example.com",
    "channels": ["email", "sms"],
    "opt_in": true,
    "source": "web_form",
    "consent_type": "marketing",
    "lawful_basis": "consent",
    "purpose": "Weekly product newsletter and order updates",
    "consent_text_version": "tos-2026-04",
    "consent_proof_url": "https://example.com/proofs/abc123.png"
  }'
```

Devuelve `201 Created`:

```json theme={null}
{
  "contact_id": "cnt_9f…",
  "consent_record_ids": ["cr_a1…", "cr_b2…"],
  "channels": ["email", "sms"],
  "state": "opted_in",
  "valid_until": null
}
```

| Campo                       | Tipo                  | Notas                                                                                                                                  |
| --------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `contact_id` / `identifier` | string                | Proporcione uno de los dos.                                                                                                            |
| `channels`                  | string\[]             | Uno o más del conjunto de canales; se deduplican y ordenan.                                                                            |
| `opt_in`                    | boolean               | **Obligatorio.** `true` = opt-in, `false` = opt-out.                                                                                   |
| `source`                    | string                | Cómo se capturó el consentimiento (p. ej. `web_form`, `import`, `double_opt_in`). Valor predeterminado `consent_api`.                  |
| `consent_type`              | string                | Categoría de propósito, p. ej. `marketing`, `transactional`. Valor predeterminado `messaging`.                                         |
| `lawful_basis`              | enum                  | Base del artículo 6 del GDPR: `consent`, `contract`, `legal_obligation`, `vital_interests`, `public_task`, `legitimate_interests`.     |
| `purpose`                   | string                | Texto libre que describe el uso (≤ 500).                                                                                               |
| `consent_text_version`      | string                | Versión del aviso que aceptó el titular.                                                                                               |
| `consent_proof_url`         | string (url)          | Enlace a una captura de pantalla o a un documento firmado.                                                                             |
| `valid_until`               | string (ISO-8601 UTC) | Instant exactly en que el consentimiento expira. Solo para opt-in — se ignora en opt-out. Mutuamente excluyente con `expires_in_days`. |
| `expires_in_days`           | integer               | Ventana de validez relativa (1–3650 días a partir de ahora). Solo para opt-in. Mutuamente excluyente con `valid_until`.                |
| `metadata`                  | object                | Claves/valores personalizados arbitrarios.                                                                                             |

Proporcionar **ambos** valores `valid_until` y `expires_in_days` es
ambiguo y se rechaza con `422 VALIDATION_ERROR`. Cuando define una
ventana, la respuesta `201` refleja el valor resuelto de
`valid_until` (el instante absoluto de expiración); es `null` para
una concesión que no expira o para un opt-out. Volver a registrar un
opt-in con una ventana nueva **amplía** la validez — el `granted_at`
original se conserva, pero la expiración se actualiza.

**Qué hace una escritura.** Cada canal registrado actualiza cuatro
superficies sincronizadas: la tabla de auditoría `consent_records`,
el espejo `channel_preferences` del contacto (la ruta rápida de
lectura que sus envíos consultan), la `suppression_list` (en un
opt-out) y una barrera STOP de corta duración en Redis para que los
lotes de campañas en curso respeten el cambio en \~10 minutos.

<Note>
  Las escrituras son **parcialmente seguras**: si un canal falla, los
  demás se aplican igualmente. Compare `consent_record_ids.length` con
  el número de canales que solicitó para detectar una escritura
  parcial. Volver a registrar un opt-in para un canal que ya tiene
  opt-in actualiza los metadatos/prueba pero **conserva el
  `granted_at` original**.
</Note>

***

## Consulta del consentimiento

`GET /compliance/consent/lookup` devuelve el estado actual de un par
`(contacto, canal)` — utilícelo como puerta previa al envío.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/compliance/consent/lookup?identifier=jordan@example.com&channel=sms" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

```json theme={null}
{
  "contact_id": "cnt_9f…",
  "channel": "sms",
  "state": "opted_in",
  "source": "web_form",
  "granted_at": "2026-04-02T10:11:00.000Z",
  "revoked_at": null,
  "lawful_basis": "consent",
  "purpose": "Weekly product newsletter and order updates",
  "consent_text_version": "tos-2026-04",
  "consent_proof_url": "https://example.com/proofs/abc123.png",
  "valid_until": null,
  "expired": false,
  "requires_reconfirmation": false
}
```

Un `state` de valor `unknown` significa que no existe registro para
el par — su aplicación decide si eso implica consentimiento (algunos
flujos transaccionales) o bloquea el envío (la mayoría de los flujos
de marketing).

Los tres últimos campos informan sobre consentimiento con duración
limitada y están **siempre presentes**:

| Campo                     | Tipo           | Notas                                                                                                                                                       |
| ------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `valid_until`             | string \| null | El instante de expiración de la concesión, o `null` cuando el consentimiento nunca expira (o cuando el par está en opt-out).                                |
| `expired`                 | boolean        | `true` cuando la concesión es opt-in pero su `valid_until` ya quedó en el pasado. Trate una concesión expirada como ya no consentida en su puerta de envío. |
| `requires_reconfirmation` | boolean        | Refleja `expired` — una sugerencia para activar un flujo de nueva autorización. Límpielo registrando un opt-in nuevo (opcionalmente con una ventana nueva). |

***

## Búsqueda de consentimientos por expirar

`GET /compliance/consent/expiring` recorre el tenant en busca de
opt-ins cuya ventana de validez haya vencido o esté a punto de
vencer — la entrada para una campaña de nueva autorización
(re-confirmación). Solo se devuelven concesiones que lleven un
`valid_until`; el consentimiento que no expira nunca aparece.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/compliance/consent/expiring?within_days=30&status=all" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

Parámetros de consulta:

| Parámetro     | Tipo    | Notas                                                                                                                                                                                               |
| ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `within_days` | integer | Horizonte de anticipación (0–3650, valor predeterminado 30). Devuelve concesiones cuyo `valid_until` sea igual o anterior a ahora + `within_days`; las concesiones ya vencidas se incluyen siempre. |
| `channel`     | enum    | Opcional — limita a un solo canal.                                                                                                                                                                  |
| `status`      | enum    | `all` (valor predeterminado), `expired` (ya pasó `valid_until`) o `expiring` (todavía válido pero dentro del horizonte).                                                                            |
| `limit`       | integer | Tamaño de página (1–100, valor predeterminado 50).                                                                                                                                                  |
| `cursor`      | string  | Cursor de paginación opaco — reenvíelo literalmente.                                                                                                                                                |

```json theme={null}
{
  "within_days": 30,
  "channel": null,
  "status": "all",
  "as_of": "2026-05-01T09:00:00.000Z",
  "items": [
    {
      "id": "cr_b2…",
      "contact_id": "cnt_9f…",
      "channel": "sms",
      "consent_type": "marketing",
      "source": "web_form",
      "lawful_basis": "consent",
      "granted_at": "2025-05-02T10:11:00.000Z",
      "valid_until": "2026-04-20T00:00:00.000Z",
      "expired": true,
      "status": "expired",
      "requires_reconfirmation": true
    }
  ],
  "next_cursor": null
}
```

Los elementos se ordenan primero por el más antiguo en expirar. Cada
uno lleva `status` (`expired` o `expiring`) para que pueda separar
"hay que reconfirmar ahora" de "avisar antes de que se cierre la
ventana". La reconfirmación es un opt-in ordinario de
`POST /compliance/consent` — opcionalmente con un `valid_until` o un
`expires_in_days` nuevos.

<Tip>
  Trate `next_cursor` como opaco y reenvíelo literalmente; un valor
  `null` significa la última página. Un cursor no válido o anticuado
  se trata como una primera página nueva en lugar de un error.
</Tip>

***

## Handshakes de consentimiento confirmado (doble opt-in)

Un `POST /compliance/consent` sencillo afirma la concesión — es el
sistema de registro una vez que su propia superficie ha obtenido el
consentimiento. Cuando el nivel de evidencia exige una **respuesta
del destinatario** registrada (consentimiento escrito expreso de la
TCPA, opt-in confirmado de la UE, revisión de campañas 10DLC),
utilice en su lugar el **handshake de doble opt-in** gestionado:

1. `POST /compliance/consent/double-opt-in` — **inicio**: registra
   una fila *pendiente* (todavía no es una concesión de
   consentimiento) y devuelve el texto de la pregunta de
   confirmación para el par.
2. El destinatario responde; usted reenvía el texto a
   `POST /compliance/consent/double-opt-in/confirm` —
   **confirmación**: una palabra clave afirmativa que coincida con
   la pregunta pendiente convierte el par en una concesión
   confirmada de `opted_in`.
3. `GET /compliance/consent/double-opt-in/status` — **lectura**:
   el estado actual (`opted_in` | `opted_out` | `pending` | `none`)
   más los indicadores `confirmed` / `awaiting_reply`, sin efectos
   secundarios.

Los handshakes confirmados llegan al mismo libro mayor de
consentimiento que documenta esta página — `/lookup`, `/history` y
la exportación los leen de forma idéntica. Hasta que se confirma,
un handshake pendiente no es una concesión de consentimiento.
Propiedad del tenant: nada inicia un handshake en nombre de la
plataforma. Mecánica completa en
[Handshakes de consentimiento confirmado (doble opt-in)](/compliance/double-opt-in).

***

## Historial de consentimiento

`GET /compliance/consent/history` devuelve el registro de auditoría
completo y paginado de un contacto — cada concesión y revocación, de
la más reciente a la más antigua.

Parámetros de consulta: `contact_id` o `identifier` (uno
obligatorio), un filtro opcional de `channel`, `limit` (≤ 100,
valor predeterminado 50) y un `cursor` opaco.

```json theme={null}
{
  "contact_id": "cnt_9f…",
  "channel": null,
  "items": [
    {
      "id": "cr_b2…",
      "channel": "sms",
      "consent_state": "opted_in",
      "granted": true,
      "source": "web_form",
      "granted_at": "2026-04-02T10:11:00.000Z",
      "revoked_at": null,
      "lawful_basis": "consent",
      "created_at": "2026-04-02T10:11:00.000Z"
    }
  ],
  "next_cursor": "eyJ0…"
}
```

<Tip>
  Trate `next_cursor` como opaco — reenvíelo literalmente para
  obtener la página siguiente. Un cursor no válido o anticuado se
  trata como una primera página nueva en lugar de un error.
</Tip>

***

## Exportación de la prueba de registro del consentimiento

`GET /compliance/consent/export` descarga el registro de
consentimiento de todo su tenant en un solo archivo — la respuesta
a una auditoría de la TCPA, a la carga de la prueba del artículo
7(1) del GDPR o a una solicitud de descubrimiento probatorio
("muestre quién aceptó o rechazó el consentimiento, cuándo, en qué
canal y desde qué fuente"). Es la contraparte en masa de `/lookup`
y `/history`.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/compliance/consent/export?format=csv&state=opted_out" \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -o consent-proof-of-record.csv
```

Parámetros de consulta:

| Parámetro     | Tipo    | Notas                                                                                                               |
| ------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `format`      | enum    | `csv` (valor predeterminado — RFC-4180, se abre en una hoja de cálculo) o `json`.                                   |
| `channel`     | enum    | Limitar a un canal.                                                                                                 |
| `state`       | enum    | `all` (valor predeterminado), `opted_in`, `opted_out` o `unknown`.                                                  |
| `contact_id`  | string  | Limita la exportación a un solo contacto — la forma habitual de una solicitud de descubrimiento probatorio.         |
| `from` / `to` | string  | Rango de fechas sobre el `created_at` del registro. Acepta una fecha suelta `YYYY-MM-DD` o una fecha-hora RFC-3339. |
| `limit`       | integer | Filas a incluir (1–50 000, valor predeterminado 50 000).                                                            |

Cada fila contiene un evento de consentimiento unido a los
identificadores del contacto — `record_id`, `contact_id`, `email`,
`phone`, `whatsapp_id`, `channel`, `consent_state`, `granted`,
`consent_type`, `source`, además de las columnas de carga de la
prueba del GDPR `lawful_basis`, `purpose`, `policy_template`,
`consent_text_version`, `consent_proof_url`, `ip_address`,
`valid_until` y las marcas de tiempo de concesión/revocación/
actualización.

Las descargas CSV llegan con un nombre de archivo fechado
(`consent-proof-of-record-YYYY-MM-DD.csv`) y nunca cruzan una
caché de lectura (`Cache-Control: no-store`). Solicite
`format=json` y la respuesta devuelve en su lugar una envoltura
`columns` / `items` / `count` — los mismos datos para consumidores
programáticos.

El acceso está restringido a claves de **propietario** y
**administrador** — la carga útil expone identificadores de
destinatarios sin procesar de todo el tenant, el mismo nivel de
confianza que la importación de supresión. Cada ejecución de
exportación se escribe a su vez en el registro de auditoría con
sus filtros y su número de filas.

<Note>
  Cuando su libro mayor supera las 50 000 filas, la exportación se
  trunca en ese tope: las respuestas CSV llevan una cabecera
  `X-Export-Truncated: true` y la envoltura JSON establece
  `truncated: true`. Acote por canal o por estado, o bien pagine
  exportando ventanas de fechas consecutivas con `from`/`to`.
</Note>

***

La **Ley de Protección de Datos Personales Digitales (DPDP)** de la
India introduce el concepto de **Consent Manager** — un
intermediario registrado y responsable que emite **recibos de
consentimiento firmados criptográficamente** en nombre de un
titular de datos. Orbit puede registrar los gestores por los que
pasan sus usuarios y verificar los recibos que emiten.

### Registrar un Consent Manager

`POST /compliance/consent/managers` (administrador/propietario)
registra un gestor y almacena su clave pública (una PEM SPKI de
ECDSA P-256) que se usa para verificar cada recibo que firma.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/consent/managers \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Consent Manager",
    "manager_id": "acme-cm-001",
    "manager_url": "https://cm.acme.example",
    "public_key": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----",
    "country_code": "IN"
  }'
```

* `GET /compliance/consent/managers` enumera los gestores
  registrados (los activos primero).
* `PUT /compliance/consent/managers/{id}` actualiza o desactiva uno
  (actualización parcial; todos los campos opcionales).

### Almacenar un recibo firmado

`POST /compliance/consent/receipts` verifica un recibo firmado por
un gestor y lo persiste como consentimiento. La firma (ECDSA P-256
/ SHA-256, IEEE-P1363, base64url) se comprueba contra la clave
pública del gestor registrado sobre una canonización JSON de claves
ordenadas inspirada en JCS del payload **antes** de almacenar
cualquier cosa. Esta canonización ordena las claves de objetos en
orden ascendente según la unidad de código UTF-16 y elimina los
espacios en blanco no significativos, pero no es una implementación
completa de RFC 8785 — en particular, no aplica las reglas de
serialización de números que exige JCS. Firme los recibos con la
misma forma de claves ordenadas que usa Orbit en lugar de asumir
que un verificador RFC 8785 completo según la especificación
producirá un hash coincidente.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/consent/receipts \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "cnt_9f…",
    "consent_manager_id": "acme-cm-001",
    "channel": "sms",
    "consent_type": "marketing",
    "receipt": {
      "receipt_id": "rcpt_77…",
      "issued_at": "2026-05-01T09:00:00.000Z",
      "purpose": "Promotional SMS",
      "fiduciary_id": "fid_acme",
      "signature": "MEUCIQ…",
      "payload": { "…": "…" }
    }
  }'
```

Devuelve `201` con `{ "id": …, "receipt_id": …, "verified": true
}`. Una firma no válida, o un gestor no registrado o inactivo,
devuelve `422 CONSENT_RECEIPT_INVALID` — el detalle señala que el
payload puede haber sido manipulado o que el gestor puede haber
rotado sus claves.

### Reverificar un recibo almacenado

`POST /compliance/consent/receipts/{id}/verify` vuelve a comprobar
un recibo almacenado previamente contra la clave **actual** del
gestor — úselo durante una auditoría para confirmar que un recibo
sigue validando y si su gestor permanece activo. `{id}` acepta
tanto el id del registro de consentimiento como el `receipt_id`.

<Note>
  Los recibos de consentimiento requieren la migración
  `consent_managers` del tenant. En tenants anteriores a ella, las
  rutas de lectura se degradan con elegancia: la lista de gestores
  devuelve una lista vacía y el endpoint de reverificación devuelve
  `404`. La emisión de un recibo es fail-closed, por lo que
  `POST /compliance/consent/receipts` devuelve `422
      CONSENT_RECEIPT_INVALID` en tenants previos a la migración en
  lugar de degradarse — ejecute la migración antes de emitir
  recibos.
</Note>

***

## Referencias relacionadas

* [Ensamblar una postura GDPR de extremo a extremo](/compliance/gdpr-posture-guide) —
  la secuencia que alimenta esta capa de consentimiento.
* [Handshakes de consentimiento confirmado (doble opt-in)](/compliance/double-opt-in) —
  el flujo begin/confirm/status por encima de un registro de
  consentimiento sencillo.
* [Postura de consentimiento: las políticas de consentimiento desconocido](/compliance/consent-default-policy) —
  los controles de nivel de organización que deciden qué pueden
  recibir los contactos sin fila en el libro mayor (envíos de
  marketing frente a salida del CDP).
* [Opt-Out y listas de supresión](/compliance/opt-out-suppression) —
  importación en masa de opt-outs y cómo la lista de supresión
  controla los envíos.
* [DSAR](/compliance/dsar) — atender solicitudes de acceso y
  eliminación sobre el registro de consentimiento.
* [Onboarding de DLT-India](/compliance/dlt-india) — la capa de
  registro que se combina con el consentimiento DPDP en los SMS de
  la India.
* [Referencia de API → Compliance](/api-reference/endpoints/compliance) — esquemas
  completos de solicitud/respuesta (regenerados a partir de la API
  en vivo).
