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.
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.
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.
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.
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é.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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
| Champ | Format | Obligatoire | Description |
|---|---|---|---|
eventId | string | oui | Id stable Trustt SMS. Idempotence : rejouer le même eventId = aucun effet de bord |
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 (optout = niveau personne) |
occurredAt | ISO-8601 + offset | oui | Date de l'évènement métier |
payload | objet | selon type | Données spécifiques au type |
Payloads par type
| type | payload | Effet 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.
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.