Architecture de l'intégration Trustt / Trustt SMS

Document visuel de cadrage technique · compagnon des « Points bloquants API Trustt SMS » · 04/06/2026
Télécharger la version Markdown (.md)

Ce document explique, par des schémas, d'abord ce qu'est Trustt SMS (Partie A : ses surfaces, ses composants, son cycle de vie, son modèle de données), puis comment il s'articule avec la plateforme Trustt mère (Partie B : l'intégration, les points bloquants, les endpoints). Objectif : partager une compréhension commune et lever les ambiguïtés. Le code couleur ci-dessous s'applique à tous les diagrammes.

Existe et fonctionne Manque / bloquant À clarifier / fiabilité partielle

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.

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.

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
mission, produit, consignes"] C["/c Confirmation / check-in
l'ambassadeur confirme ou decline"] H["/h Portail hote
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;

Les surfaces publiques (vert) sont celles que voient les gens hors de l'entreprise. 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.

flowchart LR
  WEB["Next.js (web)
console admin + pages publiques"] API["API Express
routes + webhooks"] WRK["Workers BullMQ
materializer, envoi, sweeper,
rappels brief, entrant"] MDB[("MongoDB")] RDS[("Redis (files)")] PROV["Fournisseur SMS
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. C'est ce qui permet d'absorber les pics sans bloquer l'opérateur.

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

flowchart TB
  CREATE["1. Creation de la campagne
(marque, date, mode check-in)"] --> IMPORT["2. Import de l'audience
(contacts = futurs ambassadeurs)"] IMPORT --> BRIEF["3. Brief publie (optionnel)
une livraison par contact"] BRIEF --> SEQ["4. Sequence SMS
(J-3, jour J, sur retrait, relances)"] SEQ --> LAUNCH["5. Lancement"] LAUNCH --> MAT["6. Materializer
programme les SMS"] MAT --> SEND["7. Envoi SMS
(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
present / recupere (/h)"] HOST --> RETICK["10. Retick : SMS 'sur retrait',
annule les relances devenues inutiles"] VIEW --> SUB["11. Soumission du contenu publie
(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.

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

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,
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 du document (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) Comment les deux systèmes communiquent, ce qui manque, et les endpoints proposés.

1 Vue d'ensemble des deux systèmes

Trustt est le système mère (campagnes, ambassadeurs). Trustt SMS envoie les SMS de suivi. Le canal de lecture existe (API REST GET). Le canal de remontée n'existe pas encore.

flowchart LR
  subgraph TRUSTT["Trustt (plateforme mere)"]
    direction TB
    T1["Companies / Brands"]
    T2["Campaigns"]
    T3["Ambassadeurs selectionnes"]
  end
  subgraph TSMS["Trustt SMS (outil interne)"]
    direction TB
    S1["Clients / Campagnes"]
    S2["Contacts"]
    S3["Sequences SMS + Portail hote"]
  end
  TRUSTT -- "API REST lecture seule (GET) : EXISTE" --> TSMS
  TSMS -. "Remontees (optout, retrait, lien publie...) : MANQUE" .-> TRUSTT
  linkStyle 0 stroke:#16a34a,stroke-width:2px;
  linkStyle 1 stroke:#dc2626,stroke-width:2px;
    

Constat clé : tout ce que Trustt SMS doit renvoyer à Trustt (désabonnement, colis récupéré, lien de contenu publié) n'a aujourd'hui aucun canal. L'API est en lecture seule.

2 Mapping des modèles de données

Trustt a deux niveaux au-dessus de la campagne (Company puis Brand), Trustt SMS un seul (Client). Et surtout : une campagne Trustt peut alimenter plusieurs campagnes Trustt SMS.

flowchart TB
  subgraph L["Modele Trustt (mere)"]
    direction TB
    CO["Company
companyUUID"] --> BR["Brand
brandUUID"] BR --> CA["Campaign
campaignUUID"] CA --> AM["Ambassador
candidateUUID"] end subgraph R["Modele Trustt SMS"] direction TB CL["Client"] --> CP["Campagne"] CP --> JX["CampaignContact
(jonction)"] JX --> CT["Contact
telephone UNIQUE"] end CO -. "aplati dans" .-> CL BR -. "champ sur la campagne" .-> CP CA -- "1 vers N" --> CP AM -. "candidateUUID = cle de deduplication" .-> CT classDef hot fill:#fdf3e3,stroke:#b45309,color:#1a1f29; class CA,CP hot;

Piège de champ : dans le payload ambassadeur, le champ nommé phoneE164 contient du national (0612345678), c'est phone qui est au format E.164 (+33612345678). Trustt SMS impose un téléphone unique : deux ambassadeurs partageant un numéro entrent en collision (cf. doc, points B5 et D2).

3 Parcours d'appel en lecture (import)

Pour atteindre les ambassadeurs d'une campagne, il faut descendre la hiérarchie en quatre appels. Aucun filtre incrémental : à chaque cycle, la liste complète est re-téléchargée.

sequenceDiagram
  autonumber
  participant S as Trustt SMS
  participant A as API Trustt (GET)
  S->>A: GET /companies (key)
  A-->>S: data [companyUUID, nom]
  S->>A: GET /brands (key, coid)
  A-->>S: data [brandUUID, publicName]
  S->>A: GET /campaigns (key, bid)
  A-->>S: data [campaignUUID, name, type, progress]
  S->>A: GET /ambassadors (key, caid)
  A-->>S: data [candidateUUID, phone, receiptDate, testEndDate]
  Note over S,A: Pas de delta, pas de pagination, pas de webhook
    

À clarifier (D6, D7, D9) : un filtre « modifié depuis », une liste plate des campagnes éligibles, ou un webhook éviteraient de re-balayer toute la hiérarchie à chaque synchronisation.

4 Flux d'import et automatisation des relances

Une fois les ambassadeurs récupérés, le mapping alimente la base Trustt SMS. La date de réception colis (receiptDate) sert de pivot par contact : la séquence de SMS se planifie alors automatiquement.

flowchart LR
  P["Pull ambassadeurs
d'une campagne"] --> M{"Mapping"} M --> M1["phone vers phoneE164
(et non le champ phoneE164)"] M --> M2["candidateUUID vers externalTrusttId
cle de deduplication"] M --> M3["receiptDate vers pivot par contact"] M1 --> DB[("Base Trustt SMS")] M2 --> DB M3 --> DB DB --> SEQ["Moteur de sequence
relances auto sur receiptDate / testEndDate"] classDef good fill:#e9f8ef,stroke:#16a34a,color:#1a1f29; class SEQ good;

Gain (demande F1) : la plomberie « pivot par contact » existe déjà côté Trustt SMS. Si receiptDate est fiable et horodatée, les relances « J+2 après réception », « J+5 rédige ton avis » se déclenchent sans saisie manuelle.

5 Remontées Trustt SMS vers Trustt (le canal manquant)

Plusieurs signaux produits par Trustt SMS doivent remonter. Plutôt que dix endpoints, nous proposons un puits d'évènements unique, idempotent et extensible.

flowchart LR
  subgraph PROD["Trustt SMS produit"]
    direction TB
    E1["optout (STOP)"]
    E2["pickup / reception colis"]
    E3["content_submission
lien de contenu publie"] E4["sms_status / numero invalide"] E5["sentiment, attendance, brief_viewed"] end E1 --> SINK E2 --> SINK E3 --> SINK E4 --> SINK E5 --> SINK SINK["POST /events
puits unique (A CREER cote Trustt)"] -.-> TR["Trustt (mere)"] classDef missing fill:#fdeceb,stroke:#dc2626,color:#1a1f29; class SINK missing;

Bloquant W1 : sans surface d'écriture, aucune de ces remontées n'est possible. Chaque évènement porte candidateUUID (+ campaignUUID si pertinent) et un eventId stable pour l'idempotence.

6 Le problème d'attribution d'un SMS entrant

Un SMS entrant ne contient que le numéro, pas la campagne visée. Comme le numéro est partagé entre plusieurs campagnes, le rattachement automatique est une heuristique, pas une certitude.

flowchart TB
  R["SMS entrant : seulement le numero"] --> Q{"Quelle campagne ?"}
  Q --> H["Heuristique : campagne du dernier SMS
sortant de moins de 7 jours, hors archivee"] H --> OK["FIABLE si une seule campagne active"] H --> F1["FAUX si 2 campagnes actives en parallele"] H --> F2["AUCUN si reponse de plus de 7 jours"] H --> F3["AUCUN si derniere campagne archivee"] classDef bad fill:#fdeceb,stroke:#dc2626,color:#1a1f29; classDef good fill:#e9f8ef,stroke:#16a34a,color:#1a1f29; class F1,F2,F3 bad; class OK good;

Conséquences : l'historique sortant est rattachable de façon fiable par campagne ; l'historique entrant ne l'est qu'au mieux. Donc un historique par personne est fiable, un historique par campagne côté entrant est indicatif (cf. W3). Le désabonnement n'est pas concerné : il est de toute façon au niveau personne.

Cas du lien de contenu publié (W4) : aujourd'hui l'opérateur copie le lien reçu et le colle à la main dans Trustt. En lui faisant confirmer la campagne au moment de pousser le lien, le rattachement redevient fiable, en un clic.

7 Désabonnement : niveau personne et symétrie

Un STOP reçu sur une campagne doit couper l'envoi pour ce numéro partout, des deux côtés. C'est une exigence RGPD, pas une option.

flowchart TB
  STOP["Ambassadeur repond STOP
(sur une campagne)"] --> G["Trustt SMS : optout GLOBAL
niveau personne / numero"] G --> C1["Campagne A : coupe"] G --> C2["Campagne B : coupe"] G --> C3["Campagne C : coupe"] G --> W["POST /events optout"] W --> TR["Trustt coupe ce numero
sur toutes ses campagnes"] TR -. "symetrie : si optout cote Trustt" .-> IMP["expose a l'import
pour ne pas recontacter"] classDef good fill:#e9f8ef,stroke:#16a34a,color:#1a1f29; classDef missing fill:#fdeceb,stroke:#dc2626,color:#1a1f29; class C1,C2,C3 good; class W,TR missing;

Bloquant W2 + symétrie : si un ambassadeur se désinscrit côté Trustt, ce statut doit être exposé à l'import (point B2) pour qu'on ne le recontacte pas. Sans les deux sens, les systèmes divergent sur le même numéro.

8 Campagnes événement : confirmation de présence (masterclass)

Une campagne Trustt de type Evènement (masterclass, atelier) suit la même logique de « check-in » que le retrait colis, avec deux sous-signaux : l'intention (confirme ou décline, avant) et la présence réelle (jour J).

flowchart TB
  TY["Campagne Trustt de type Evenement
(typeId 3, ex : masterclass)"] --> NEED["Trustt SMS a besoin :
date + lieu de l'event (F3, F4)"] NEED --> PIV["date de l'event = pivot des relances"] PIV --> P1["J-3 : 'confirme ta venue'"] P1 --> PORT["Portail public (lien /c)
l'ambassadeur repond"] PORT --> CONF["intention : confirmed / declined"] CONF --> JJ["Jour J : presence reelle marquee
present / absent"] JJ --> EV["Remontee : attendance + presence
POST /events (A CREER)"] classDef good fill:#e9f8ef,stroke:#16a34a,color:#1a1f29; classDef missing fill:#fdeceb,stroke:#dc2626,color:#1a1f29; class PORT,CONF,JJ good; class EV missing;

Même famille que le retrait : retrait colis (présent / récupéré) et présence à un événement (confirmé / présent) partagent le mécanisme de check-in. Côté Trustt SMS, c'est le champ checkInMode (pickup ou attendance) qui décide lequel s'applique, dérivé du type de campagne Trustt.

Bloquant identique à B1 / W1 : la confirmation de présence et la présence réelle ne peuvent pas remonter sans surface d'écriture. Côté lecture, il nous faut la date et le lieu de l'événement (F3, F4) pour piloter les relances de confirmation.

9 Spécification des endpoints manquants (proposition de contrat)

Contrat proposé, dans le style de votre documentation, pour que l'implémentation soit directe. Un endpoint d'écriture (le manque principal) et un endpoint de lecture optionnel qui simplifie la synchronisation.

POST /api_trusttsms/events À CRÉER

Puits d'évènements unique : Trustt SMS y pousse tous les signaux qu'il produit (désabonnement, retrait, présence, lien publié, statut SMS, sentiment). Idempotent et extensible.

POST https://app.trustt.io/api_trusttsms/events
Authorization: Bearer VOTRE_CLE_API   (recommande, cf. D10 ; ?key= accepte)
Content-Type: application/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

ChampFormatObligatoireDescription
eventIdstringouiId stable Trustt SMS. Idempotence : rejouer le même eventId = aucun effet de bord
typeenumouioptout, pickup, attendance, content_submission, sms_status, sentiment, brief_viewed
candidateUUIDUUIDouiAmbassadeur concerné
campaignUUIDUUIDoui sauf optoutCampagne concernée (optout = niveau personne)
occurredAtISO-8601 + offsetouiDate de l'évènement métier
payloadobjetselon typeDonnées spécifiques au type

Payloads par type

typepayloadEffet attendu côté Trustt
optout{ phone, reason }Couper le SMS pour ce numéro sur toutes les campagnes
pickup{ status: "picked_up" }Enregistrer le retrait (maj receiptStatus / receiptDate)
attendance{ intent, presence }Présence à l'évènement (confirmé/décliné + présent/absent)
content_submission{ url, attribution }Rattacher le lien de contenu publié au livrable
sms_status{ status }Qualité de la donnée (numéro mort)
sentiment{ rating, comment }Feedback marque sur l'ambassadeur
brief_viewed{ }Ambassadeur a ouvert le brief

Réponse 200 : { "data": [ { "eventId": "...", "status": "accepted | duplicate" } ] } · Erreurs : 401 (clé), 422 (champ / enum), 404 (UUID inconnu), 500.

GET /api_trusttsms/eligible_campaigns OPTIONNEL

Liste plate des campagnes éligibles pour la clé, company et brand inline : remplace la cascade companies puis brands puis campaigns (D7). Le paramètre updatedSince permet la synchronisation incrémentale (D6).

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

{
  "data": [ {
    "campaignUUID": "d4e5f6a7-...",
    "companyUUID": "a1b2c3d4-...", "companyNom": "L'Oreal France",
    "brandUUID": "c3d4e5f6-...", "brandPublicName": "Garnier",
    "type": { "typeId": 3, "libelle": "Evenement" },
    "progress": { "progressId": 6, "libelle": "..." },
    "updatedAt": "2026-06-01T10:00:00+02:00"
  } ]
}

Confort : remplace 1 + N + (N x M) appels par un seul, et rend la synchro incrémentale possible. Les endpoints existants restent inchangés.

Sans l'endpoint d'écriture, rien ne remonte. C'est le bloquant W1 : retrait, présence, désabonnement et lien publié en dépendent tous. Un seul endpoint les débloque ensemble.