# Points bloquants et clarifications - API Trustt SMS

Document de retour technique à l'équipe Trustt ayant rédigé l'API.
Réf. doc source : « Documentation API Trustt SMS », version 27/05/2026.
Auteur du retour : équipe Trustt SMS (intégration). Date : 04/06/2026.

## But de ce document

L'outil Trustt SMS doit (1) importer les ambassadeurs sélectionnés d'une campagne Trustt pour leur envoyer des SMS de suivi, et (2) faire remonter à Trustt l'information « colis récupéré ».
La lecture complète de l'API soulève des points qui empêchent ou fragilisent cette intégration. Ils sont classés par sévérité :

- **BLOQUANT** : empêche l'intégration ou crée un risque légal / des données fausses.
- **À CORRIGER** : incohérence ou erreur dans le document, peu coûteuse à fixer.
- **CLARIFICATION** : information manquante nécessaire pour cadrer l'intégration.

Le document est en deux parties : la **Partie A** décrit l'architecture de Trustt SMS (pour situer où l'intégration se branche), la **Partie B** liste les points bloquants et les demandes côté API Trustt.

## Sommaire

**Partie A - Architecture de Trustt SMS**

- [A1. Surfaces et acteurs](#a1-surfaces-et-acteurs)
- [A2. Composants techniques (runtime)](#a2-composants-techniques-runtime)
- [A3. Cycle de vie d'une campagne (bout en bout)](#a3-cycle-de-vie-dune-campagne-bout-en-bout)
- [A4. Modèle de données (entités principales)](#a4-modèle-de-données-entités-principales)
- [A5. Où l'intégration Trustt se branche](#a5-où-lintégration-trustt-se-branche)

**Partie B - Intégration avec Trustt (mère)**

- [1. Bloquants](#1-bloquants)
- [2. À corriger dans le document](#2-à-corriger-dans-le-document)
- [3. Clarifications nécessaires](#3-clarifications-nécessaires)
- [4. Besoins d'écriture (remontées Trustt SMS -> Trustt)](#4-besoins-décriture-remontées-trustt-sms---trustt)
- [5. Demandes d'enrichissement (non bloquantes, mais à fort effet)](#5-demandes-denrichissement-non-bloquantes-mais-à-fort-effet)
- [6. Récapitulatif priorisé](#6-récapitulatif-priorisé)

---

# Partie A - Architecture de Trustt SMS

Ce qu'est l'outil, indépendamment de l'intégration : ses surfaces, ses composants, son cycle de vie et son modèle de données. Les diagrammes ci-dessous se rendent nativement sur GitHub / GitLab (blocs `mermaid`).

## A1. Surfaces et acteurs

Trustt SMS expose trois pages publiques (accès par lien token, sans login) et une console admin (Google OAuth). Chaque surface s'adresse à un acteur différent.

```mermaid
flowchart TB
  AMB["Ambassadeur"]
  STAFF["Staff hote (lieu / evenement)"]
  OP["Operateur Trustt SMS"]
  subgraph PUB["Surfaces publiques (lien token, sans login)"]
    direction LR
    B["/b  Brief ambassadeur<br/>mission, produit, consignes"]
    C["/c  Confirmation / check-in<br/>l'ambassadeur confirme ou decline"]
    H["/h  Portail hote<br/>le staff marque present / recupere"]
  end
  subgraph ADMIN["Console admin (Google OAuth @trustt.io)"]
    direction LR
    AD1["Campagnes + sequences SMS"]
    AD2["Audience / import contacts"]
    AD3["Conversations (SMS entrants)"]
    AD4["Briefs + soumissions"]
  end
  AMB --> B
  AMB --> C
  STAFF --> H
  OP --> ADMIN
  classDef good fill:#e9f8ef,stroke:#16a34a,color:#1a1f29;
  class B,C,H good;
```

Un portail hôte unifié `/h/c` regroupe plusieurs campagnes d'un même lieu sur une seule page.

## A2. Composants techniques (runtime)

Deux processus (API web + workers de file), une base Mongo, une file Redis, un fournisseur SMS interchangeable. Le fournisseur renvoie les statuts de livraison et les SMS entrants par webhook.

```mermaid
flowchart LR
  WEB["Next.js (web)<br/>console admin + pages publiques"]
  API["API Express<br/>routes + webhooks"]
  WRK["Workers BullMQ<br/>materializer, envoi, sweeper,<br/>rappels brief, entrant"]
  MDB[("MongoDB")]
  RDS[("Redis (files)")]
  PROV["Fournisseur SMS<br/>Twilio / Vonage / OVH"]
  WEB --> API
  API --> MDB
  API --> RDS
  RDS --> WRK
  WRK --> MDB
  WRK --> PROV
  PROV -. "statut livraison + SMS entrant (webhook)" .-> API
```

Les envois ne partent jamais sur le chemin d'une requête : ils passent par des files Redis avec retries, throttle 1 SMS/s, et un sweeper qui récupère les messages bloqués.

## A3. Cycle de vie d'une campagne (bout en bout)

Le parcours complet, de la création par l'opérateur jusqu'aux réactions de l'ambassadeur et au suivi. C'est ici que l'intégration Trustt vient se brancher (import en entrée, remontées en sortie).

```mermaid
flowchart TB
  CREATE["1. Creation de la campagne<br/>(marque, date, mode check-in)"] --> IMPORT["2. Import de l'audience<br/>(contacts = futurs ambassadeurs)"]
  IMPORT --> BRIEF["3. Brief publie (optionnel)<br/>une livraison par contact"]
  BRIEF --> SEQ["4. Sequence SMS<br/>(J-3, jour J, sur retrait, relances)"]
  SEQ --> LAUNCH["5. Lancement"]
  LAUNCH --> MAT["6. Materializer<br/>programme les SMS"]
  MAT --> SEND["7. Envoi SMS<br/>(worker + fournisseur)"]
  SEND --> AMB{"8. Reaction ambassadeur"}
  AMB -->|"ouvre /b"| VIEW["consulte le brief"]
  AMB -->|"clique /c"| CONF["confirme / decline"]
  AMB -->|"repond par SMS"| INB["conversation entrante"]
  CONF --> HOST["9. Jour J : le staff marque<br/>present / recupere (/h)"]
  HOST --> RETICK["10. Retick : SMS 'sur retrait',<br/>annule les relances devenues inutiles"]
  VIEW --> SUB["11. Soumission du contenu publie<br/>(lien du post + capture)"]
  INB --> CONV["L'operateur repond (conversations)"]
  RETICK --> THANKS["SMS de remerciement"]
  classDef good fill:#e9f8ef,stroke:#16a34a,color:#1a1f29;
  class CONF,HOST,SUB good;
```

**Soumission de contenu (étape 11) :** Trustt SMS a déjà une entité « soumission » (lien du post + plateforme Instagram/TikTok/YouTube + validation opérateur). C'est exactement le lien de contenu publié de la Partie B (W4), aujourd'hui re-saisi à la main dans Trustt.

## A4. Modèle de données (entités principales)

La chaîne centrale : Client puis Campagne puis la jonction CampaignContact qui relie une Campagne et un Contact, avec autour le brief, les soumissions, les SMS sortants et entrants, et le stock.

```mermaid
erDiagram
  CLIENT ||--o{ CAMPAIGN : possede
  CAMPAIGN ||--o{ CAMPAIGNCONTACT : contient
  CONTACT ||--o{ CAMPAIGNCONTACT : participe
  CAMPAIGN ||--o| BRIEF : a
  BRIEF ||--o{ BRIEFDELIVERY : livre
  CONTACT ||--o{ BRIEFDELIVERY : recoit
  BRIEFDELIVERY ||--o| SUBMISSION : preuve
  CAMPAIGNCONTACT ||--o{ MESSAGE : sortant
  CONTACT ||--o{ INBOUNDMESSAGE : entrant
  CAMPAIGN ||--o{ INVENTORY : stock
  CONTACT {
    string phoneE164 UK
    string firstName
    bool unsubscribed
  }
  CAMPAIGNCONTACT {
    date pivotDate
    string attendanceResponse
    string markedByHostType
  }
  CAMPAIGN {
    string status
    string checkInMode
    object flowConfig
  }
  SUBMISSION {
    string postUrl
    string platform
    string status
  }
```

**Points d'ancrage de l'intégration :** `CONTACT.phoneE164` est unique (un contact = un numéro) ; `CAMPAIGNCONTACT.pivotDate` est la date par contact (cible de `receiptDate`) ; `markedByHostType` porte le retrait / la présence ; `SUBMISSION.postUrl` porte le lien publié.

## A5. Où l'intégration Trustt se branche

Deux points de contact seulement : l'import des ambassadeurs en entrée (existe), et les remontées en sortie (à créer). Tout le reste de Trustt SMS reste inchangé.

```mermaid
flowchart LR
  subgraph TR["Trustt (mere)"]
    direction TB
    AMB2["Ambassadeurs selectionnes"]
    REC["receiptStatus / receiptDate"]
  end
  subgraph TS["Trustt SMS"]
    direction TB
    CT2["Contacts + CampaignContact"]
    OUT["Marques hote, optout,<br/>soumissions de contenu"]
  end
  AMB2 -- "IMPORT (GET ambassadors)" --> CT2
  OUT -. "REMONTEE (POST /events, a creer)" .-> TR
  classDef good fill:#e9f8ef,stroke:#16a34a,color:#1a1f29;
  classDef missing fill:#fdeceb,stroke:#dc2626,color:#1a1f29;
  class CT2 good;
  class OUT missing;
  linkStyle 0 stroke:#16a34a,stroke-width:2px;
  linkStyle 1 stroke:#dc2626,stroke-width:2px;
```

La suite (Partie B) détaille ces deux points : l'import via les 4 endpoints de lecture, et les remontées via le puits d'évènements à créer.

---

# Partie B - Intégration avec Trustt (mère)

## 1. Bloquants

### B1. L'API est en lecture seule : la remontée « colis récupéré » n'a pas de canal

L'API est documentée comme « REST en lecture seule (GET) ». Or notre besoin métier inclut de **renvoyer à Trustt** l'information qu'un colis a été récupéré (retrait confirmé côté Trustt SMS par un opérateur ou un point de retrait).

Aucun endpoint d'écriture (POST/PUT/PATCH) n'existe.

**Ce qu'on demande :**
- Soit l'existence (ou la création) d'un endpoint d'écriture pour pousser un évènement de retrait / réception, avec son contrat (URL, auth, payload, idempotence).
- Soit la confirmation que `receiptStatus` est calculé et fait foi **côté Trustt** (logistique propre), auquel cas aucune remontée n'est attendue de notre part. Dans ce cas, préciser comment Trustt apprend qu'un colis a été retiré.

Note : ce besoin de retrait n'est qu'un cas particulier d'un besoin plus large de remontée (désabonnement, historique des échanges, engagement). Voir la **section 4 - Besoins d'écriture**, qui propose une surface d'écriture unique couvrant tous ces signaux.

### B2. Aucun statut de consentement SMS dans le payload ambassadeur

Le payload `ambassadors` expose `email`, `phone`, identité et dates de mission, mais **aucune information de consentement** : pas d'opt-in SMS, pas de date de recueil du consentement, pas de canal autorisé, pas de statut de désinscription.

Envoyer un SMS commercial ou de service suppose une base légale (RGPD ; juridictions FR / LU / BE concernées). Sans donnée de consentement transmise par Trustt, nous ne pouvons pas établir que l'ambassadeur a accepté d'être contacté par SMS.

**Ce qu'on demande :**
- L'ambassadeur sélectionné a-t-il consenti à être contacté par SMS dans le cadre de la mission ? Sur quelle base (CGU programme, opt-in explicite) ?
- Pouvez-vous exposer dans le payload : un booléen de consentement SMS, sa date, et le cas échéant un statut de désinscription (l'ambassadeur s'est désabonné côté Trustt) ?

### B3. Sémantique et énumération de `receiptStatus` non spécifiées

Le document mentionne seulement, en creux : « si `receiptStatus = 0` ou pas de date, `receiptDate` et `testEndDate` peuvent être null ». L'exemple montre `receiptStatus: 1`. Mais :

- L'ensemble des valeurs possibles n'est pas donné (uniquement 0 et 1 ? d'autres états : en transit, retourné, refusé ?).
- La sémantique exacte n'est pas définie : « reçu » signifie reçu par le transporteur, livré, retiré en point relais, ou confirmé par l'ambassadeur ?
- Le moment où le statut passe à 1 n'est pas précisé.

C'est central pour nous : c'est le signal le plus proche de « colis récupéré », et il conditionne le déclenchement de nos relances.

**Ce qu'on demande :** l'énumération complète des valeurs de `receiptStatus`, leur signification, et l'évènement métier qui fait basculer chaque valeur.

### B4. Contradiction sur le mapping `progressId` -> libellé

L'exemple de réponse de l'endpoint Campaigns indique :

```json
"progress": { "progressId": 6, "libelle": "Sélection du panel" }
```

Mais la table de référence du même endpoint indique :

```
4  Inscription en cours
5  Sélection du panel
6  Produits à envoyer
7  Test en cours
8  Terminée
```

Donc l'exemple dit `6 = Sélection du panel`, la table dit `6 = Produits à envoyer` et `5 = Sélection du panel`. Les deux ne peuvent pas être vrais.

Nous comptons utiliser `progressId` pour filtrer les campagnes pertinentes (typiquement « produits à envoyer » à « terminée »). Un mapping faux nous ferait cibler les mauvaises campagnes.

**Ce qu'on demande :** la table `progressId -> libellé` faisant autorité, et la correction de l'exemple ou de la table.

### B5. Le champ nommé `phoneE164` n'est pas au format E.164

Dans l'exemple ambassadeur :

```json
"phoneE164": "0612345678",
"phone": "+33612345678"
```

Le champ `phoneE164` contient un numéro **national** (`0612345678`), tandis que le champ `phone` contient le format **E.164** (`+33612345678`). Les noms sont donc inversés par rapport à leur contenu.

Notre base stocke le téléphone au format E.164 strict (`+...`). Si nous mappons le champ par son nom, tous les numéros sont cassés.

**Ce qu'on demande :**
- Confirmer quel champ est **garanti** au format E.164 international avec préfixe `+`.
- Confirmer que ce champ est toujours rempli et valide pour un testeur sélectionné (cf. aussi C-D ci-dessous sur les numéros étrangers).
- Idéalement, renommer les champs pour que le nom corresponde au contenu, ou documenter explicitement l'inversion.

---

## 2. À corriger dans le document

### C1. Libellés de colonnes mélangés dans l'endpoint Campaigns

Les deux tables de valeurs (progress et type) ont toutes les deux l'en-tête « typeId | Libellé ». La première liste en réalité des `progressId` (4 à 8). L'en-tête de la première table devrait être « progressId ».

### C2. Description erronée du paramètre `caid` (endpoint Ambassadors)

La table des paramètres décrit `caid` comme « Identifiant campagne (brandUUID) ». Ce doit être `campaignUUID`, pas `brandUUID`. Erreur de copier-coller probable.

### C3. Format de date non ISO et sans fuseau horaire

Les dates sont au format `"2026-04-15 14:30:00"` (espace, pas de `T`, pas d'offset). Deux problèmes :

- Pas de fuseau : `testEndDate: "...23:59:59"` est-il en heure de Paris, en UTC, autre ? Nous planifions les envois en heure de Paris ; une ambiguïté de fuseau décale les relances de 1 à 2 heures.
- Format non ISO-8601, parsing plus fragile.

**Ce qu'on demande :** préciser le fuseau, idéalement passer en ISO-8601 avec offset (`2026-04-15T14:30:00+02:00`).

### C4. Encodage des caractères

Les valeurs contiennent des accents et apostrophes (`L'Oréal France`, `Dupont`). Merci de confirmer l'encodage `UTF-8` des réponses et que l'apostrophe de `L'Oréal` est un caractère littéral et non une séquence échappée.

### C5. Corps d'erreur 500 non structuré

Le document indique que le corps d'une 500 est « variable selon le contexte ». Nous ne pouvons pas le parser de façon fiable. Merci d'appliquer la même enveloppe `{"error": "..."}` que les autres codes, y compris en 5xx.

---

## 3. Clarifications nécessaires

### D1. Stabilité et unicité de `candidateUUID`

Nous souhaitons utiliser `candidateUUID` comme clé de déduplication primaire d'un ambassadeur (plutôt que le téléphone).

**Questions :** `candidateUUID` est-il stable dans le temps pour une même personne, identique d'une campagne à l'autre pour le même ambassadeur, et jamais réattribué ?

### D2. Deux ambassadeurs avec le même numéro de téléphone

Notre base impose un index **unique** sur le téléphone (un contact = un numéro). Si Trustt peut renvoyer deux `candidateUUID` distincts partageant le même `phone` (couple, ligne familiale ou professionnelle partagée), notre modèle entre en collision.

**Questions :** ce cas existe-t-il dans Trustt ? Le téléphone est-il unique par ambassadeur côté Trustt, ou plusieurs ambassadeurs peuvent-ils partager un numéro ?

### D3. Dates `receiptDate` / `testEndDate` : par campagne ou par personne ?

Un ambassadeur peut être sur plusieurs campagnes. Nous supposons que `receiptDate` et `testEndDate` sont propres au couple (campagne, ambassadeur), pas à la personne.

**Question :** confirmer que ces dates sont bien par participation (campagne × candidat) et non globales à la personne.

### D4. Désélection d'un ambassadeur après import

L'API renvoie « les testeurs sélectionnés ». Si un ambassadeur est retiré du panel après que nous l'avons importé, nous continuerions à lui envoyer des SMS.

**Questions :** un ambassadeur sélectionné peut-il être retiré ensuite ? Si oui, comment le détectons-nous (disparition de la liste au prochain appel ? statut dédié ?) ?

### D5. Transition d'une campagne vers archivée (9) ou annulée (99)

Le document indique que ces campagnes ne sont plus éligibles (404 sur Ambassadors). Si une campagne que nous suivions passe en annulée, elle disparaît silencieusement.

**Questions :** confirmer le passage en 404 lors de la transition. Existe-t-il un signal explicite « campagne annulée » pour que nous arrêtions immédiatement les envois en cours plutôt que de le déduire d'un 404 ?

### D6. Pas de synchronisation incrémentale (delta)

Aucun paramètre `since` / `updatedAt`, aucune pagination, aucun horodatage sur les entités. Pour détecter les nouveaux ambassadeurs d'un panel, nous devons re-télécharger la liste complète à chaque cycle.

**Questions :** pouvez-vous exposer un filtre « modifié depuis » et/ou un horodatage `updatedAt` par entité ? Une pagination est-elle prévue pour les très gros panels ?

### D7. Pas de liste plate des campagnes éligibles

Pour énumérer toutes les campagnes éligibles d'une clé, nous devons parcourir `companies` -> `brands` -> `campaigns` (cascade d'appels). Sur de nombreux clients/marques, cela multiplie les requêtes.

**Question :** pouvez-vous exposer une liste plate des campagnes éligibles pour une clé (avec `companyUUID` et `brandUUID` inline), pour éviter la cascade ?

### D8. Limites de débit et fréquence de polling recommandée

Comme tout est en pull et que nous voulons une réconciliation périodique, il nous faut connaître les contraintes.

**Questions :** existe-t-il une limite de débit (rate limit) ? Quelle fréquence de polling recommandez-vous ? Le journal `ApiLog` impose-t-il des contraintes d'usage ?

### D9. Webhook / push côté Trustt

Tout est en pull. Un nouvel ambassadeur ajouté au panel, un changement de `receiptStatus`, ne nous parviennent qu'au prochain sondage.

**Question :** un mécanisme de webhook (sélection d'un ambassadeur, changement de statut de réception) est-il envisageable pour éviter le polling ?

### D10. Authentification par clé en query string

La clé d'API passe en paramètre d'URL (`?key=...`). Une clé en URL se retrouve dans les journaux d'accès, proxies et historiques, ce qui est un anti-pattern de gestion de secret. Le document précise par ailleurs que chaque appel est journalisé (`ApiLog`).

**Questions :**
- Pouvez-vous accepter la clé via un en-tête `Authorization` plutôt qu'en query string ?
- Quelle est la procédure de rotation de la clé en cas de compromission ?
- La clé est-elle distincte entre environnements (production / test) ?

### D11. Environnement de test (sandbox)

Seule l'URL de production (`https://app.trustt.io/`) est documentée. Nous avons besoin de construire et tester l'intégration sans toucher aux données réelles ni polluer `ApiLog`.

**Question :** existe-t-il un environnement de recette (URL + jeu de données de test + clé dédiée) ?

### D12. Versionnement de l'API

Le préfixe `api_trusttsms/` n'est pas versionné. Un changement de contrat (renommage de champ, changement d'énum) nous casserait silencieusement.

**Questions :** quelle est la politique de versionnement et de dépréciation ? Comment serons-nous notifiés des changements de contrat ?

---

## 4. Besoins d'écriture (remontées Trustt SMS -> Trustt)

L'API actuelle est en lecture seule. Or Trustt SMS produit plusieurs signaux qui doivent **remonter** à Trustt. Aucun canal n'existe aujourd'hui (même racine que B1). Plutôt qu'une dizaine d'endpoints dédiés, nous recommandons une surface d'écriture **minimale et extensible**.

### Design recommandé : un puits d'évènements unique

`POST /api_trusttsms/events` (auth par en-tête, cf. D10), corps JSON :

```json
{
  "eventId": "ttsms-evt-7f3a...",
  "type": "optout | pickup | sms_status | attendance | inbound_reply | sentiment | video_sent | brief_viewed",
  "candidateUUID": "e5f6a7b8-...",
  "campaignUUID": "d4e5f6a7-...",
  "occurredAt": "2026-06-04T14:30:00+02:00",
  "payload": { }
}
```

Avantages : une seule route à maintenir côté Trustt, extensible sans casser le contrat, idempotente. Des endpoints dédiés (`/optout`, `/pickup`, ...) nous conviennent aussi si vous préférez ; l'essentiel est qu'une surface d'écriture existe.

### Évènements que Trustt SMS émettrait

| Type | Priorité | Déclencheur côté Trustt SMS | Action attendue côté Trustt | RGPD |
| :---- | :---- | :---- | :---- | :---- |
| `optout` | **Haute** | Un ambassadeur répond STOP, ou est désinscrit manuellement | **Couper le SMS pour ce numéro sur TOUTES les campagnes** + ne plus le proposer comme contactable | Obligation légale (retrait du consentement) |
| `pickup` (= B1) | **Haute** | Retrait / réception confirmé côté Trustt SMS | Mettre à jour `receiptStatus` / `receiptDate` | - |
| `sms_status` | **Haute** (numéro invalide), Moyenne (reste) | Numéro définitivement rejeté par l'opérateur, ou statut de livraison | Marquer la donnée de contact comme obsolète (qualité de base) | - |
| `attendance` | Moyenne-haute | Campagnes évènement (masterclass, atelier) : intention (confirmé / décliné) puis présence réelle (présent / absent) | Suivi de présence à l'évènement | - |
| `inbound_reply` | Moyenne | L'ambassadeur répond par SMS | Trace de l'échange | Corps = donnée perso (voir ci-dessous) |
| `sentiment` | Moyenne | Note opérateur (négatif / neutre / positif + commentaire) | Feedback marque sur l'ambassadeur | Donnée perso / opinion |
| `content_submission` | Moyenne-haute | Un ambassadeur renvoie par SMS le lien de son contenu publié | Rattacher ce lien au livrable de l'ambassadeur sur la campagne (remplace l'ajout manuel) | URL de la publication |
| `brief_viewed` | Basse | L'ambassadeur a ouvert le brief | Engagement | - |

### 4.1 Désabonnement : le point le plus sensible

C'est ta priorité, et c'est légalement contraignant. Précisions :

- Le désabonnement est au niveau **personne / numéro**, jamais campagne. Le même numéro étant partagé entre plusieurs campagnes, un STOP doit couper **partout** côté Trustt, pas seulement sur la campagne d'origine.
- **Symétrie indispensable** : si un ambassadeur se désinscrit côté Trustt (ou côté Trustt SMS), l'autre système doit le savoir. Concrètement, exposez un statut de désinscription dans le payload `ambassadors` (cf. B2) pour qu'à l'import nous ne recontactions pas quelqu'un qui a déjà dit STOP. Sans cette symétrie, les deux systèmes divergent sur le même numéro et un désabonné peut être re-sollicité (faute RGPD).

### 4.2 Historique des échanges : format à cadrer, et limite d'attribution par campagne

Nous disposons de l'historique complet : SMS sortants (corps rendu, statut, date d'envoi, date de livraison, nombre de segments) et entrants (corps, date de réception). Deux formats possibles :

1. **Transcript complet**, message par message (volumineux ; le corps est une donnée personnelle).
2. **Résumé par ambassadeur** (nombre envoyés, livrés, réponses, date du dernier échange).

**Question :** lequel voulez-vous ? Si transcript complet, merci de confirmer la base légale et la durée de conservation côté Trustt. Par principe de minimisation, nous préférons par défaut remonter les **métadonnées** (statut, dates, compteurs) plutôt que le **corps** des messages, sauf besoin explicite de votre part.

**Limite importante à connaître - l'attribution par campagne n'est pas fiable dans le sens entrant.** Le numéro de téléphone étant partagé entre plusieurs campagnes (un même ambassadeur peut participer à plusieurs missions), nous ne savons pas toujours rattacher un message à LA bonne campagne :

- **SMS sortants** : rattachement fiable. Nous les générons depuis la séquence d'une campagne précise.
- **SMS entrants (réponses)** : rattachement **heuristique seulement**. Une réponse ne contient que le numéro, pas la campagne visée. Nous l'attribuons à la campagne du dernier SMS sortant vers ce numéro, dans une fenêtre de 7 jours, hors campagnes archivées. Ce n'est **pas fiable à 100 %** :
  - si le numéro est actif sur deux campagnes en même temps, la réponse peut être attribuée à la mauvaise ;
  - si la réponse arrive plus de 7 jours après le dernier envoi, elle n'est rattachée à aucune campagne ;
  - si la dernière campagne est archivée, le rattachement est abandonné.

**Conséquence pour vous :** un historique **par personne** (clé `candidateUUID` / numéro) est fiable ; un historique **par campagne** côté entrant est au mieux indicatif. Nous recommandons donc de remonter l'historique au **niveau personne**, ou, si vous tenez au découpage par campagne, d'accepter que nous marquions chaque message entrant avec un **niveau de confiance d'attribution** (certain pour le sortant, heuristique pour l'entrant) que vous ne devez pas traiter comme certain. À noter : ceci n'affecte **pas** le désabonnement (W2), qui est de toute façon au niveau personne et coupe partout.

### 4.3 Lien de contenu publié renvoyé par l'ambassadeur (automatiser un ajout aujourd'hui manuel)

Le cas réel (le titre « lien vidéo » était trompeur) :

- L'ambassadeur doit publier un contenu (post, vidéo) dans le cadre de sa mission.
- Trustt SMS le relance pour savoir s'il a publié. Certains ne l'ont pas fait ; quand ils le font, ils **répondent par SMS avec le lien** de leur publication (la preuve du livrable).
- Aujourd'hui, l'opérateur copie ce lien à la main, ouvre la plateforme Trustt, retrouve la campagne et l'ambassadeur, et colle le lien. Tâche manuelle, répétitive, source d'erreur.

**Objectif :** pousser automatiquement le **lien reçu** vers Trustt, rattaché à la **bonne campagne** et au **bon contact**.

**Ce qu'on demande :** un endpoint d'écriture acceptant un lien de contenu publié pour un couple (`candidateUUID`, `campaignUUID`) - typiquement l'évènement `content_submission` du puits d'écriture (section 4). Merci de préciser : le **champ cible** côté Trustt où ce lien doit atterrir, et si **plusieurs liens** par ambassadeur sont permis (publication sur plusieurs réseaux).

**Sur l'attribution (point clé) :** le lien arrive par SMS entrant, donc le rattachement automatique à une campagne est **heuristique** (cf. la limite décrite en 4.2). Mais comme la tâche est aujourd'hui réalisée par un opérateur, nous pouvons lui faire **confirmer la campagne** au moment de pousser le lien, ce qui rend le rattachement **fiable** (équivalent à ce qu'il fait déjà à la main, en un clic). Deux modes, à votre préférence :

- **Confirmé par l'opérateur** (recommandé) : fiable. L'opérateur valide campagne + contact avant l'envoi.
- **Automatique** : nous détectons le lien dans le SMS et l'attribuons par heuristique, en marquant l'attribution comme non certaine.

Côté Trustt SMS, nous prévoyons de détecter automatiquement l'URL dans la réponse et de proposer à l'opérateur un bouton « envoyer à Trustt » pré-rempli (campagne + contact), pour remplacer le copier-coller actuel.

### 4.4 Confirmation de présence aux événements (même famille que le retrait)

Une partie des campagnes sont des **événements** (type `typeId 3` côté Trustt : masterclass, atelier). Trustt SMS gère nativement la présence, sur le même mécanisme de « check-in » que le retrait colis, avec **deux sous-signaux** :

- **Intention** : l'ambassadeur confirme ou décline sa venue via un portail public (avant l'événement). Relancé automatiquement s'il ne répond pas (« n'oublie pas de confirmer ta venue »).
- **Présence réelle** : marquée le jour J (présent / absent).

**Côté écriture :** ces deux signaux doivent remonter (évènement `attendance`), exactement comme le retrait (`pickup`). Question pour vous : où la confirmation et la présence doivent-elles atterrir côté Trustt (champ cible sur le tester) ?

**Côté lecture :** pour piloter les relances de confirmation, il nous faut la **date** de l'événement (qui sert de pivot, cf. F3) et son **lieu** (cf. F4). L'endpoint `campaigns` ne les expose pas aujourd'hui.

### 4.5 Exigences transverses pour toute écriture

- **Identité** : `candidateUUID` obligatoire ; `campaignUUID` quand l'évènement est lié à une participation.
- **Idempotence** : `eventId` stable côté Trustt SMS ; rejouer le même évènement = no-op 200 (nous réessayons en cas d'erreur réseau).
- **Horodatage** : ISO-8601 avec offset.
- **Auth** : en-tête plutôt que clé en query string (cf. D10).
- **Accusé** : 200 avec écho de `eventId` ; 4xx clair en cas de rejet définitif (pour qu'on n'entre pas en boucle de retry).

### 4.6 Spécification proposée des endpoints manquants

Voici le contrat que nous proposons, dans le style de votre documentation, pour que vous puissiez l'implémenter directement. Deux endpoints : un endpoint d'écriture (le manque principal) et un endpoint de lecture optionnel qui simplifie la synchronisation.

#### Endpoint d'écriture - `POST /api_trusttsms/events`

**Description :** puits d'évènements unique permettant à Trustt SMS de remonter les signaux qu'il produit. Idempotent et extensible.

**Requête :**

```
POST https://app.trustt.io/api_trusttsms/events
Authorization: Bearer VOTRE_CLE_API      (recommandé, cf. D10 ; ?key= accepté pour cohérence avec la lecture)
Content-Type: application/json
```

**Corps** (un évènement, ou un tableau pour le batch) :

```json
{
  "eventId": "ttsms-3f9a1c7e",
  "type": "pickup",
  "candidateUUID": "e5f6a7b8-c9d0-1234-ef01-345678901234",
  "campaignUUID": "d4e5f6a7-b8c9-0123-def0-234567890123",
  "occurredAt": "2026-06-04T14:30:00+02:00",
  "payload": { "status": "picked_up" }
}
```

**Champs communs :**

| Champ | Format | Obligatoire | Description |
| :---- | :---- | :---- | :---- |
| eventId | string | oui | Id stable côté Trustt SMS. Idempotence : rejouer le même eventId ne doit produire aucun effet de bord supplémentaire |
| type | enum | oui | `optout`, `pickup`, `attendance`, `content_submission`, `sms_status`, `sentiment`, `brief_viewed` |
| candidateUUID | UUID | oui | Ambassadeur concerné |
| campaignUUID | UUID | oui sauf `optout` | Campagne concernée (l'optout est au niveau personne) |
| occurredAt | ISO-8601 + offset | oui | Date de l'évènement métier |
| payload | objet | selon type | Données spécifiques au type (ci-dessous) |

**Payloads par type :**

| type | payload | Effet attendu côté Trustt |
| :---- | :---- | :---- |
| `optout` | `{ "phone": "+33...", "reason": "sms_stop\|manual\|bounce" }` | Couper le SMS pour ce numéro sur **toutes** les campagnes, ne plus le proposer comme contactable |
| `pickup` | `{ "status": "picked_up" }` | Enregistrer le retrait / la réception (mise à jour `receiptStatus` / `receiptDate`) |
| `attendance` | `{ "intent": "confirmed\|declined", "presence": "present\|absent" }` (au moins un des deux) | Suivi de présence à l'évènement (masterclass) |
| `content_submission` | `{ "url": "https://...", "attribution": "operator_confirmed\|heuristic" }` | Rattacher le lien de contenu publié au livrable de l'ambassadeur |
| `sms_status` | `{ "status": "delivered\|failed\|invalid_number" }` | Qualité de la donnée de contact (numéro mort) |
| `sentiment` | `{ "rating": "negative\|neutral\|positive", "comment": "..." }` | Feedback marque sur l'ambassadeur |
| `brief_viewed` | `{ }` | L'ambassadeur a ouvert le brief |

**Réponse (succès) - HTTP 200 :**

```json
{ "data": [ { "eventId": "ttsms-3f9a1c7e", "status": "accepted" } ] }
```

`status` vaut `accepted` (pris en compte) ou `duplicate` (eventId déjà reçu, no-op idempotent).

**Erreurs :**

| Code | Quand |
| :---- | :---- |
| 401 | Clé absente ou invalide |
| 422 | Champ obligatoire manquant, ou type / enum invalide |
| 404 | `candidateUUID` ou `campaignUUID` inconnu |
| 500 | Erreur serveur (enveloppe `{"error": "..."}`) |

#### Endpoint de lecture optionnel - `GET /api_trusttsms/eligible_campaigns`

**Description :** liste plate de toutes les campagnes éligibles pour la clé, avec company et brand inline, pour éviter la cascade `companies` -> `brands` -> `campaigns` (cf. D7). Le paramètre optionnel `updatedSince` adresse la synchronisation incrémentale (cf. D6).

**Requête :**

```
GET https://app.trustt.io/api_trusttsms/eligible_campaigns?key=xxx
GET https://app.trustt.io/api_trusttsms/eligible_campaigns?key=xxx&updatedSince=2026-06-01T00:00:00+02:00
```

**Réponse (succès) - HTTP 200 :**

```json
{
  "data": [
    {
      "campaignUUID": "d4e5f6a7-b8c9-0123-def0-234567890123",
      "name": "Crème solaire SPF50 - testeurs",
      "companyUUID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "companyNom": "L'Oréal France",
      "brandUUID": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "brandPublicName": "Garnier",
      "type": { "typeId": 1, "libelle": "Produit" },
      "progress": { "progressId": 6, "libelle": "Produits à envoyer" },
      "updatedAt": "2026-06-01T10:00:00+02:00"
    }
  ]
}
```

Cet endpoint est un confort : il remplace 1 + N + (N x M) appels par un seul, et rend la synchronisation incrémentale possible. Les endpoints existants restent inchangés.

---

## 5. Demandes d'enrichissement (non bloquantes, mais à fort effet)

Ces demandes ne bloquent pas l'import de base, mais chacune débloque une fonctionnalité concrète côté Trustt SMS. Classées par effet décroissant. Même format que le reste : constat, usage, demande.

### F1. Fiabiliser `receiptDate` et `testEndDate` pour piloter l'automatisation

**Constat :** ces deux dates sont déjà renvoyées par l'endpoint `ambassadors`, mais sans fuseau (cf. C3) ni garantie de fiabilité (cf. B3 sur `receiptStatus`).

**Usage :** c'est la demande à plus fort effet. Trustt SMS sait déjà ancrer une séquence de SMS sur une date **par contact**. Si `receiptDate` (réception du colis) est fiable et horodatée, toute la relance se planifie automatiquement : « J+2 après réception, pense à tester », « J+5, rédige ton avis ». `testEndDate` sert d'échéance (« ta mission se termine le X »). Sans elles, l'opérateur saisit tout à la main.

**Demande :** garantir le remplissage de `receiptDate` / `testEndDate` dès que `receiptStatus` est positif, avec fuseau explicite (ISO-8601 + offset), et documenter à quel évènement métier chaque date est posée.

### F2. Langue / locale de l'ambassadeur

**Constat :** le payload `ambassadors` ne contient aucune indication de langue.

**Usage :** Trustt SMS gère l'envoi en FR / EN / DE / IT, avec une langue par contact. Sur des campagnes LU / BE multilingues, sans cette information nous envoyons tout dans la langue par défaut de la campagne.

**Demande :** ajouter un champ langue / locale par ambassadeur (ex. `fr`, `fr-BE`, `de`).

### F3. Dates au niveau campagne (début, fin de test, fenêtre de retrait)

**Constat :** l'endpoint `campaigns` ne renvoie aucune date.

**Usage :** pour les relances « au niveau campagne » (toute la cohorte au même moment), il faut une date pivot de campagne. Aujourd'hui nous ne pouvons l'inférer qu'à partir des dates individuelles des ambassadeurs.

**Demande :** exposer sur `campaigns` les dates clés de la mission (début, fin de test, fenêtre de retrait, **ou date / heure de l'événement** pour les campagnes type masterclass), avec fuseau. La date de l'événement sert de pivot aux relances de confirmation de présence (cf. 4.4).

### F4. Adresse / point de retrait

**Constat :** aucune adresse n'est exposée.

**Usage :** sur les campagnes de retrait, Trustt SMS affiche le lieu (portail opérateur, contenu du SMS). Sans adresse, l'opérateur la ressaisit.

**Demande :** exposer l'adresse de la campagne quand elle en a une (point relais de retrait, **ou lieu de l'événement** pour les masterclass / ateliers).

### F5. Métadonnées produit (nom, marque, visuel)

**Constat :** l'endpoint `campaigns` ne renvoie que `name`, `type`, `progress`.

**Usage :** Trustt SMS personnalise le SMS et le brief avec le nom du produit, la marque et un visuel. Ces données existent côté Trustt mais ne sont pas exposées.

**Demande :** exposer les métadonnées produit de la campagne (nom, marque, URL d'image).

### F6. Date de consentement SMS

**Constat / Demande :** voir B2 (bloquant). Au-delà du booléen de consentement, exposer sa **date** de recueil sert la traçabilité RGPD côté Trustt SMS (preuve de la base légale d'envoi).

### Récapitulatif des enrichissements

| Réf | Donnée demandée | Endpoint | Effet débloqué |
| :---- | :---- | :---- | :---- |
| F1 | `receiptDate` / `testEndDate` fiables + horodatées | ambassadors | Automatisation complète des relances (effet majeur) |
| F2 | Langue / locale | ambassadors | SMS multilingue par contact |
| F3 | Dates de campagne | campaigns | Pivot de campagne pour relances groupées |
| F4 | Adresse / point de retrait | campaigns | Affichage du lieu de retrait |
| F5 | Métadonnées produit | campaigns | Personnalisation SMS + brief |
| F6 | Date de consentement | ambassadors | Traçabilité RGPD |

---

## 6. Récapitulatif priorisé

| Réf | Sévérité | Sujet | Décision attendue de Trustt |
| :---- | :---- | :---- | :---- |
| B1 | Bloquant | Remontée « colis récupéré » impossible (read-only) | Endpoint d'écriture, ou confirmer que `receiptStatus` fait foi |
| B2 | Bloquant | Pas de consentement SMS | Exposer consentement + date + désinscription |
| B3 | Bloquant | `receiptStatus` non spécifié | Énum + sémantique + déclencheur |
| B4 | Bloquant | Contradiction `progressId` | Table faisant autorité |
| B5 | Bloquant | Champ E.164 mal nommé | Confirmer le champ E.164 garanti |
| W1 | Bloquant | Aucune surface d'écriture pour les remontées | Créer `POST /events` (contrat détaillé en 4.6) |
| W2 | Bloquant | Désabonnement cross-campagne | Couper le numéro partout + symétrie à l'import |
| W3 | Clarification | Format de l'historique des échanges | Transcript complet ou résumé ? base légale + rétention |
| W4 | Clarification | Remontée du lien de contenu publié (aujourd'hui ajout manuel) | Endpoint `content_submission` : champ cible + attribution confirmée par l'opérateur |
| C1-C5 | À corriger | Erreurs et ambiguïtés du document | Corrections rédactionnelles |
| D1-D5 | Clarification | Identité, dates, cycle de vie | Réponses métier |
| D6-D12 | Clarification | Protocole (delta, débit, auth, sandbox, version) | Réponses techniques |
| F1-F6 | Enrichissement | Données qui débloquent des fonctionnalités (auto-relances, multilingue, produit) | Exposer si possible, par effet décroissant |

---

## Annexe : ce que nous comprenons de l'API (à valider)

Pour lever tout malentendu, voici notre compréhension actuelle. Merci de corriger si nécessaire.

- Hiérarchie : `Company` (entreprise contractante Community Builder) -> `Brand` (marque de production) -> `Campaign` (mission) -> `Ambassador` (testeur sélectionné).
- Les « contacts » que nous importons sont les ambassadeurs **sélectionnés** d'une campagne, récupérés campagne par campagne via `caid`.
- Une liste vide est un succès (200), pas un 404.
- Auth par `key` en query string, HTTPS obligatoire, GET uniquement.
- `receiptDate` = date de réception du colis par l'ambassadeur ; `testEndDate` = échéance de la mission. Nous comptons utiliser `receiptDate` comme date pivot par contact pour déclencher automatiquement la séquence de SMS de suivi.
