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

# Gestion du consentement et reçus

> Enregistrez, recherchez et auditez le consentement de messagerie par canal sur Orbit — y compris le suivi du fondement licite RGPD et les reçus signés de Consent Manager DPDP pour l'Inde.

# Gestion du consentement et reçus

Avant de contacter un destinataire sur un canal réglementé, vous avez
généralement besoin d'un fondement licite — le plus souvent le
**consentement**. L'API de consentement d'Orbit est le système de
référence pour savoir qui a opté pour l'adhésion ou la désinscription,
sur quel canal, quand, et sous quel fondement juridique. Chaque
écriture se propage vers les surfaces contre lesquelles vos envois
sont bloqués, de sorte qu'enregistrer le consentement ici est ce qui
débloque (ou bloque) réellement un message. Vous pouvez aussi
[exporter la piste complète](#exporting-the-consent-proof-of-record)
sous forme de fichier CSV ou JSON prêt pour l'audit.

Tous les points d'entrée ci-dessous sont racinés sur
`https://api.orbit.devotel.io/api/v1/compliance`.

<Warning>
  L'enregistrement du consentement dans Orbit crée une piste
  auditable, mais ne rend pas par lui-même un envoi licite. Vous
  restez responsable de l'obtention d'un consentement valide et du
  contenu que vous envoyez. Cette page n'est pas un avis juridique.
</Warning>

***

## Canaux et états

Le consentement est suivi **par canal**. L'ensemble des canaux pris en
charge est :

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

Une paire `(contact, canal)` se résout en l'un des trois états :

| État        | Signification                                                                                                     |
| ----------- | ----------------------------------------------------------------------------------------------------------------- |
| `opted_in`  | Consentement accordé et non révoqué.                                                                              |
| `opted_out` | Consentement révoqué, ou une désinscription explicite enregistrée.                                                |
| `unknown`   | Aucun enregistrement de consentement n'existe pour la paire — votre porte d'envoi décide de la valeur par défaut. |

***

## Enregistrement du consentement

`POST /compliance/consent` enregistre une adhésion ou une
désinscription sur un ou plusieurs canaux en un seul appel. Identifiez
le contact par `contact_id` **ou** par `identifier` (un e-mail, un
téléphone E.164 ou un identifiant WhatsApp — Orbit résout le type
automatiquement).

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

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

| Champ                       | Type                  | Notes                                                                                                                                               |
| --------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contact_id` / `identifier` | string                | Fournissez l'un des deux.                                                                                                                           |
| `channels`                  | string\[]             | Un ou plusieurs canaux de l'ensemble ; dédupliqués et triés.                                                                                        |
| `opt_in`                    | boolean               | **Requis.** `true` = adhésion, `false` = désinscription.                                                                                            |
| `source`                    | string                | Comment le consentement a été capturé (p. ex. `web_form`, `import`, `double_opt_in`). Défaut `consent_api`.                                         |
| `consent_type`              | string                | Catégorie de finalité, p. ex. `marketing`, `transactional`. Défaut `messaging`.                                                                     |
| `lawful_basis`              | enum                  | Fondement RGPD Art 6 : `consent`, `contract`, `legal_obligation`, `vital_interests`, `public_task`, `legitimate_interests`.                         |
| `purpose`                   | string                | Texte libre décrivant l'utilisation (≤ 500).                                                                                                        |
| `consent_text_version`      | string                | Version de la notice acceptée par la personne.                                                                                                      |
| `consent_proof_url`         | string (url)          | Lien vers une capture d'écran ou un document signé.                                                                                                 |
| `valid_until`               | string (ISO-8601 UTC) | Instant absolu auquel le consentement expire. Adhésion uniquement — ignoré lors d'une désinscription. Mutuellement exclusif avec `expires_in_days`. |
| `expires_in_days`           | integer               | Fenêtre de validité relative (1–3650 jours à partir de maintenant). Adhésion uniquement. Mutuellement exclusif avec `valid_until`.                  |
| `metadata`                  | object                | Paires clé/valeur arbitraires.                                                                                                                      |

Fournir **à la fois** `valid_until` et `expires_in_days` est ambigu et
rejeté avec `422 VALIDATION_ERROR`. Lorsque vous définissez une
fenêtre, la réponse `201` renvoie le `valid_until` résolu (l'instant
d'expiration absolu) ; il vaut `null` pour une autorisation sans
expiration ou une désinscription. Ré-enregistrer une adhésion avec une
nouvelle fenêtre **prolonge** la validité — le `granted_at` original
est conservé, mais l'expiration est mise à jour.

**Ce qu'une écriture fait.** Chaque canal enregistré met à jour
quatre surfaces synchronisées : la table d'audit `consent_records`, le
miroir `channel_preferences` du contact (le chemin rapide en lecture
que vos envois consultent), la `suppression_list` (lors d'une
désinscription), et une barrière STOP Redis de courte durée pour que
les lots de campagne en cours honorent le changement en \~10 minutes.

<Note>
  Les écritures sont **partiellement sûres** : si un canal échoue, les
  autres s'appliquent quand même. Comparez `consent_record_ids.length`
  au nombre de canaux demandés pour détecter une écriture partielle.
  Ré-enregistrer une adhésion pour un canal déjà adhéré actualise les
  métadonnées/preuves mais **conserve le `granted_at` original**.
</Note>

***

## Recherche du consentement

`GET /compliance/consent/lookup` renvoie l'état actuel d'une paire
`(contact, canal)` — utilisez-le comme porte pré-envoi.

```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 `unknown` signifie qu'aucun enregistrement n'existe pour
la paire — votre application décide si cela implique un consentement
(certain flux transactionnels) ou bloque l'envoi (la plupart des flux
marketing).

Les trois derniers champs rapportent le consentement borné dans le
temps et sont **toujours présents** :

| Champ                     | Type           | Notes                                                                                                                                                                         |
| ------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `valid_until`             | string \| null | L'instant d'expiration de l'autorisation, ou `null` lorsque le consentement n'expire jamais (ou la paire est désinscrite).                                                    |
| `expired`                 | boolean        | `true` lorsque l'autorisation est adhérée mais que son `valid_until` est désormais dans le passé. Traitez une autorisation expirée comme non consentie à votre porte d'envoi. |
| `requires_reconfirmation` | boolean        | Miroir de `expired` — un indice pour déclencher un flux de re-permission. Effacez-le en enregistrant une nouvelle adhésion (éventuellement avec une nouvelle fenêtre).        |

***

## Trouver les consentements qui expirent

`GET /compliance/consent/expiring` balaie le tenant pour les adhésions
dont la fenêtre de validité a expiré ou est sur le point de l'être —
l'entrée d'une campagne de re-permission (re-confirmation). Seules les
autorisations portant un `valid_until` sont renvoyées ; le
consentement sans expiration n'apparaît jamais.

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

Paramètres de requête :

| Paramètre     | Type    | Notes                                                                                                                                                                                                  |
| ------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `within_days` | integer | Horizon de prévision (0–3650, défaut 30). Renvoie les autorisations dont le `valid_until` est inférieur ou égal à maintenant + `within_days` ; les autorisations déjà expirées sont toujours incluses. |
| `channel`     | enum    | Optionnel — limiter à un seul canal.                                                                                                                                                                   |
| `status`      | enum    | `all` (défaut), `expired` (déjà passé `valid_until`), ou `expiring` (toujours valide mais dans l'horizon).                                                                                             |
| `limit`       | integer | Taille de page (1–100, défaut 50).                                                                                                                                                                     |
| `cursor`      | string  | Curseur de pagination opaque — renvoyez-le tel quel.                                                                                                                                                   |

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

Les éléments sont ordonnés de la plus ancienne expiration d'abord.
Chacun porte un `status` (`expired` ou `expiring`) afin que vous
puissiez séparer « doit re-confirmer maintenant » de « avertir avant
la fermeture de la fenêtre ». La re-confirmation est une adhésion
ordinaire `POST /compliance/consent` — éventuellement avec un nouveau
`valid_until` ou `expires_in_days`.

<Tip>
  Traitez `next_cursor` comme opaque et renvoyez-le tel quel ; une
  valeur `null` signifie la dernière page. Un curseur invalide ou
  périmé est traité comme une première page neuve plutôt qu'une erreur.
</Tip>

***

## Poignées de main du consentement confirmé (double opt-in)

Un simple `POST /compliance/consent` affirme l'autorisation — c'est le
système de référence une fois que votre propre surface a obtenu le
consentement. Lorsque le niveau de preuve exige une **réponse du
destinataire** enregistrée (consentement écrit exprès TCPA, opt-in
confirmé UE, revue de campagne 10DLC), pilotez plutôt la **poignée de
main de double opt-in** gérée :

1. `POST /compliance/consent/double-opt-in` — **begin** : enregistre
   une ligne *en attente* (pas encore une autorisation de consentement)
   et renvoie le texte de l'invite de confirmation pour la paire.
2. Le destinataire répond ; relayez le texte vers
   `POST /compliance/consent/double-opt-in/confirm` — **confirm** :
   un mot-clé affirmatif correspondant à l'invite en attente convertit
   la paire en une autorisation `opted_in` confirmée.
3. `GET /compliance/consent/double-opt-in/status` — **read** :
   l'état actuel (`opted_in` | `opted_out` | `pending` | `none`) plus
   les drapeaux `confirmed` / `awaiting_reply`, sans effet de bord.

Les poignées de main confirmées atterrissent dans le même registre de
consentement que cette page documente — `/lookup`, `/history` et
l'export les lisent identiquement. Tant qu'elle n'est pas confirmée,
une poignée de main en attente n'est pas une autorisation de
consentement. Tenant-owned : rien ne démarre une poignée de main pour
le compte de la plateforme. Mécanique complète sur
[Confirmed Consent (Double Opt-In) Handshakes](/compliance/double-opt-in).

***

## Historique du consentement

`GET /compliance/consent/history` renvoie la piste d'audit complète et
paginée d'un contact — chaque autorisation et révocation, la plus
récente d'abord.

Paramètres de requête : `contact_id` ou `identifier` (un requis), un
filtre `channel` optionnel, `limit` (≤ 100, défaut 50), et un `cursor`
opaque.

```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>
  Traitez `next_cursor` comme opaque — renvoyez-le tel quel pour
  récupérer la page suivante. Un curseur invalide ou périmé est traité
  comme une première page neuve plutôt qu'une erreur.
</Tip>

***

## Exporter la preuve de référence du consentement

`GET /compliance/consent/export` télécharge la piste de consentement
de l'ensemble de votre tenant en un seul fichier — la réponse à un
audit TCPA, au fardeau de preuve RGPD Art 7(1), ou à une demande de
découverte (« montrez qui a opté pour l'adhésion ou la désinscription,
quand, sur quel canal, depuis quelle source »). C'est la contrepartie
en masse de `/lookup` et `/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
```

Paramètres de requête :

| Paramètre     | Type    | Notes                                                                                                              |
| ------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `format`      | enum    | `csv` (défaut — RFC-4180, s'ouvre dans un tableur) ou `json`.                                                      |
| `channel`     | enum    | Limiter à un seul canal.                                                                                           |
| `state`       | enum    | `all` (défaut), `opted_in`, `opted_out`, ou `unknown`.                                                             |
| `contact_id`  | string  | Limiter l'export à un seul contact — la forme d'une demande de découverte.                                         |
| `from` / `to` | string  | Plage de dates sur le `created_at` de l'enregistrement. Accepte une date nue `YYYY-MM-DD` ou un datetime RFC-3339. |
| `limit`       | integer | Lignes à inclure (1–50 000, défaut 50 000).                                                                        |

Chaque ligne porte un événement de consentement joint aux identifiants
du contact — `record_id`, `contact_id`, `email`, `phone`,
`whatsapp_id`, `channel`, `consent_state`, `granted`, `consent_type`,
`source`, plus les colonnes de fardeau de preuve RGPD `lawful_basis`,
`purpose`, `policy_template`, `consent_text_version`,
`consent_proof_url`, `ip_address`, `valid_until`, et les horodatages
d'autorisation/révocation/mise à jour.

Les téléchargements CSV arrivent avec un nom de fichier daté
(`consent-proof-of-record-YYYY-MM-DD.csv`) et ne croisent jamais un
cache de lecture (`Cache-Control: no-store`). Demandez `format=json`
et la réponse renvoie plutôt une enveloppe `columns` / `items` /
`count` — les mêmes données pour les consommateurs programmatiques.

L'accès est limité aux clés **owner** et **admin** — la charge expose
les identifiants bruts des destinataires à l'échelle du tenant, le
même niveau de confiance que l'import de suppression. Chaque exécution
d'export est elle-même écrite dans le journal d'audit avec ses filtres
et son nombre de lignes.

<Note>
  Lorsque votre registre dépasse 50 000 lignes, l'export est tronqué
  au plafond : les réponses CSV portent un en-tête
  `X-Export-Truncated: true` et l'enveloppe JSON définit
  `truncated: true`. Réduisez par canal ou par état, ou paginez en
  exportant des fenêtres de dates consécutives avec `from`/`to`.
</Note>

***

Le **Digital Personal Data Protection Act (DPDP)** de l'Inde introduit
le concept de **Consent Manager** — un intermédiaire enregistré et
responsable qui émet des **reçus de consentement** cryptographiquement
signés pour le compte d'une personne concernée. Orbit peut enregistrer
les gestionnaires par lesquels passent vos utilisateurs et vérifier
les reçus qu'ils émettent.

### Enregistrer un Consent Manager

`POST /compliance/consent/managers` (admin/owner) enregistre un
gestionnaire et stocke sa clé publique (un PEM SPKI ECDSA P-256)
utilisée pour vérifier chaque reçu qu'il signe.

```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` énumère les gestionnaires
  enregistrés (actifs d'abord).
* `PUT /compliance/consent/managers/{id}` met à jour ou désactive l'un
  d'eux (mise à jour partielle ; tous les champs optionnels).

### Stocker un reçu signé

`POST /compliance/consent/receipts` vérifie un reçu signé par un
gestionnaire et le persiste comme consentement. La signature (ECDSA
P-256 / SHA-256, IEEE-P1363, base64url) est vérifiée contre la clé
publique du gestionnaire enregistré sur une canonicalisation JSON à
clés triées inspirée de JCS de la charge **avant** que quoi que ce soit
soit stocké. Cette canonicalisation trie les clés d'objet par ordre
croissant d'unité de code UTF-16 et supprime les espaces
insignifiants, mais ce n'est pas une implémentation complète de RFC
8785 — en particulier elle n'applique pas les règles de sérialisation
des nombres mandatées par JCS. Signez les reçus avec la même forme à
clés triées qu'Orbit utilise plutôt que de supposer qu'un vérificateur
RFC 8785 complet produira un hash correspondant.

```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": { "…": "…" }
    }
  }'
```

Renvoie `201` avec `{ "id": …, "receipt_id": …, "verified": true }`.
Une mauvaise signature, ou un gestionnaire non enregistré/inactif,
renvoie `422 CONSENT_RECEIPT_INVALID` — le détail note que la charge a
pu être altérée ou que le gestionnaire a pu effectuer une rotation de
clés.

### Re-vérifier un reçu stocké

`POST /compliance/consent/receipts/{id}/verify` re-vérifie un reçu
précédemment stocké contre la clé **actuelle** du gestionnaire —
utilisez-le lors d'un audit pour confirmer qu'un reçu se valide
toujours et si son gestionnaire reste actif. `{id}` accepte soit
l'identifiant de l'enregistrement de consentement, soit le
`receipt_id`.

<Note>
  Les reçus de consentement exigent la migration tenant
  `consent_managers`. Sur les tenants qui la précèdent, les chemins de
  lecture se dégradent avec grâce : la liste des gestionnaires renvoie
  une liste vide et le point de re-vérification renvoie `404`. L'émission
  d'un reçu est fail-closed, donc `POST /compliance/consent/receipts`
  renvoie `422 CONSENT_RECEIPT_INVALID` sur les tenants pré-migration
  plutôt que de se dégrader — exécutez la migration avant d'émettre des
  reçus.
</Note>

***

## Références associées

* [Assembling a GDPR Posture End to End](/compliance/gdpr-posture-guide) —
  la séquence que cette couche de consentement alimente.
* [Confirmed Consent (Double Opt-In) Handshakes](/compliance/double-opt-in) —
  le flux begin/confirm/status au-dessus d'un enregistrement de
  consentement simple.
* [Consent Posture: The Unknown-Consent Policies](/compliance/consent-default-policy) —
  les réglages au niveau de l'organisation qui décident ce que les
  contacts sans ligne au registre peuvent recevoir (envois marketing
  vs fanout CDP).
* [Opt-Out & Suppression Lists](/compliance/opt-out-suppression) —
  import en masse des désinscriptions et comment la liste de
  suppression bloque les envois.
* [DSAR](/compliance/dsar) — honorer les demandes d'accès/suppression
  sur l'enregistrement de consentement.
* [DLT-India Onboarding](/compliance/dlt-india) — la couche
  d'enregistrement qui s'associe au consentement DPDP sur les SMS
  indiens.
* [API Reference → Compliance](/api-reference/endpoints/compliance) —
  schémas complets de requêtes/réponses (régénérés depuis l'API en
  direct).
