@smsmode/rcs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,923 @@
1
+ # @smsmode/rcs
2
+
3
+ > SDK TypeScript officiel pour l'API REST smsmode© : Intégrez la messagerie RCS enrichie en moins de 5 minutes.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@smsmode/rcs)](https://www.npmjs.com/package/@smsmode/rcs)
6
+ [![Node.js](https://img.shields.io/node/v/@smsmode/rcs)](https://nodejs.org)
7
+
8
+ ---
9
+
10
+ ## Présentation
11
+
12
+ Ce SDK est une couche d'abstraction au-dessus de l'API REST smsmode©. Il vous évite de gérer manuellement les headers HTTP, l'encodage des paramètres, les erreurs et le timeout, et vous offre à la place une interface TypeScript typée avec autocomplétion IDE complète.
13
+
14
+ **Ce que le SDK prend en charge :**
15
+ - Envoi de messages RCS unitaires, en batch et programmés
16
+ - Contenu riche : texte, fichier, carte et carrousel avec suggestions interactives
17
+ - Gestion des campagnes RCS groupées (jusqu'à 1 000 destinataires)
18
+ - Réception et identification des webhooks (DLR et MO)
19
+ - Gestion des erreurs typées (400, 401, 402, 429, etc.)
20
+ - Timeout configurable avec annulation propre via `AbortController`
21
+
22
+ **Ce que le SDK ne prend pas en charge :**
23
+ - **Retry automatique** : c'est intentionnel. La stratégie de retry dépend de votre contexte métier. Un exemple est fourni dans la section [Patterns](#patterns).
24
+ - **Gestion des channels et organisations** : utilisez l'interface smsmode ou l'API directement.
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @smsmode/rcs
32
+ ```
33
+
34
+ **Prérequis :** Node.js >= 20.0.0
35
+
36
+ Le SDK utilise `fetch` natif (disponible nativement depuis Node.js 18, sans flag depuis Node.js 20) et n'a **aucune dépendance runtime**. Rien d'autre à installer.
37
+
38
+ ### ESM (recommandé)
39
+
40
+ Si votre projet utilise `"type": "module"` dans son `package.json` ou des fichiers `.mjs`, utilisez l'import ES module :
41
+
42
+ ```typescript
43
+ import { SmsmodeRcsClient } from '@smsmode/rcs';
44
+ ```
45
+
46
+ ### CommonJS
47
+
48
+ Si votre projet utilise `require()` (fichiers `.js` sans `"type": "module"`, ou `.cjs`), utilisez :
49
+
50
+ ```javascript
51
+ const { SmsmodeRcsClient } = require('@smsmode/rcs');
52
+ ```
53
+
54
+ Le package expose les deux formats : vous n'avez rien à configurer, Node.js choisit automatiquement le bon selon votre environnement.
55
+
56
+ ---
57
+
58
+ > ⚠️ **Server-side uniquement.** Ce SDK est conçu pour s'exécuter côté serveur (Node.js). Ne l'utilisez jamais dans du code exécuté dans un navigateur : votre clé API serait exposée publiquement dans le bundle JavaScript.
59
+
60
+ ---
61
+
62
+ ## Quick Start
63
+
64
+ L'exemple minimal pour envoyer votre premier message RCS :
65
+
66
+ ```typescript
67
+ import { SmsmodeRcsClient } from '@smsmode/rcs';
68
+
69
+ const client = new SmsmodeRcsClient({ apiKey: 'your-api-key' });
70
+
71
+ const message = await client.send({
72
+ recipient: { to: '33600000001' },
73
+ body: { type: 'TEXT', text: 'Bonjour depuis smsmode RCS !' },
74
+ });
75
+
76
+ console.log(message.messageId); // identifiant unique du message
77
+ console.log(message.status.value); // "ENROUTE", "DELIVERED"...
78
+ ```
79
+
80
+ > Pensez à envelopper vos appels dans un `try/catch` en production : voir la section [Gestion des erreurs](#gestion-des-erreurs).
81
+
82
+ ---
83
+
84
+ ## Configuration
85
+
86
+ ```typescript
87
+ const client = new SmsmodeRcsClient({
88
+ apiKey: 'your-api-key', // Obligatoire
89
+ timeout: 10000, // Optionnel — défaut : 10 000ms
90
+ });
91
+ ```
92
+
93
+ **`apiKey`** : Votre clé API smsmode. Voir ci-dessous comment l'obtenir et la gérer.
94
+
95
+ **`timeout`** : Durée maximale d'attente d'une réponse en millisecondes. Au-delà, la requête est annulée et une erreur est levée. Augmentez cette valeur si vous envoyez des batchs volumineux.
96
+
97
+ ### Obtenir et gérer votre clé API
98
+
99
+ **Où la trouver :**
100
+ 1. Connectez-vous à votre [espace client smsmode](https://ui.smsmode.com)
101
+ 2. Allez dans **Settings > API Keys**
102
+ 3. Créez une nouvelle clé ou copiez une clé existante
103
+
104
+ **Bonnes pratiques :**
105
+
106
+ Ne commitez jamais votre clé API dans votre dépôt Git. Utilisez une variable d'environnement :
107
+
108
+ ```bash
109
+ # fichier .env à la racine du projet
110
+ SMSMODE_API_KEY=votre_cle_api
111
+ ```
112
+
113
+ ```typescript
114
+ // Chargez la variable avec dotenv ou votre gestionnaire d'environnement
115
+ const client = new SmsmodeRcsClient({ apiKey: process.env.SMSMODE_API_KEY });
116
+ ```
117
+
118
+
119
+ **Permissions associées à une clé :**
120
+
121
+ Chaque clé API est liée à un rôle dans votre organisation smsmode :
122
+
123
+ | Rôle | Périmètre |
124
+ |------|-----------|
125
+ | User | Envoyer dans son propre Channel uniquement |
126
+ | Manager | Envoyer dans tous les Channels de son organisation |
127
+ | Administrator | Envoyer dans les Channels des sous-organisations |
128
+
129
+ Si vous recevez une erreur `401`, vérifiez que :
130
+ - La clé est bien copiée en entier (sans espace avant/après)
131
+ - La clé n'a pas été révoquée dans l'espace client
132
+ - Vous utilisez bien le header `X-Api-Key` : le SDK le gère automatiquement, mais si vous testez via curl ou Postman, c'est un point d'attention courant
133
+
134
+ ### Enregistrer votre agent RCS
135
+
136
+ En plus de votre clé API, l'envoi de messages RCS nécessite un **agent RCS** validé. Un agent RCS est l'identité d'entreprise vérifiée qui apparaît côté destinataire : nom de marque, logo, couleur. À la différence d'un expéditeur SMS libre, un agent RCS doit être **enregistré et validé par Google** avant tout envoi.
137
+
138
+ > **Prérequis bloquant :** aucun message ne peut être envoyé sans agent validé. Cette étape est à effectuer une seule fois, avant toute intégration.
139
+
140
+ **[Enregistrer mon agent RCS →](https://www.smsmode.com/enregistrement-agent-rcs/)**
141
+
142
+ Une fois votre agent validé par Google et les opérateurs, il est automatiquement utilisé pour tous vos envois. Si vous disposez de plusieurs agents et souhaitez en sélectionner un spécifique, renseignez le champ `from` dans votre requête avec le nom de l'agent tel qu'il apparaît dans la liste des agents de votre Channel (1-40 caractères).
143
+
144
+ ---
145
+
146
+ ## Format des numéros de téléphone
147
+
148
+ Tous les numéros doivent être au format **E.164** : indicatif pays suivi du numéro local, sans `+`, sans espaces, sans tirets, sans zéro initial.
149
+
150
+ | Pays | Numéro local | Format E.164 |
151
+ |------|-------------|--------------|
152
+ | France | 06 00 00 00 01 | `33600000001` |
153
+ | Belgique | 0499 00 00 01 | `32499000001` |
154
+ | Suisse | 079 000 00 01 | `41790000001` |
155
+
156
+ ```typescript
157
+ // Correct
158
+ { to: '33600000001' }
159
+
160
+ // Correct également : le + est accepté
161
+ { to: '+33600000001' }
162
+
163
+ // Incorrect : le zéro initial du numéro local ne s'inclut pas
164
+ { to: '0600000001' }
165
+
166
+ // Incorrect : pas d'espaces ni de tirets
167
+ { to: '33 6 00 00 00 01' }
168
+ ```
169
+
170
+ Un numéro mal formaté entraînera une erreur `400 Bad Request` de l'API.
171
+
172
+ > **Note RCS :** le destinataire doit disposer d'un appareil et d'un opérateur compatibles RCS. Si ce n'est pas le cas, le message ne sera pas livré : RCS ne dispose pas de fallback SMS automatique.
173
+
174
+ ---
175
+
176
+ ## Messages RCS
177
+
178
+ Un **Message** est un envoi RCS unitaire ou en batch. C'est la ressource de base de l'API smsmode RCS. Chaque message est lié à un Channel (votre agent RCS) et peut être associé à une Campaign pour le suivi statistique.
179
+
180
+ ### Envoyer un message
181
+
182
+ Envoi simple — le minimum requis.
183
+
184
+ ```typescript
185
+ await client.send({
186
+ recipient: { to: '33600000001' },
187
+ body: { type: 'TEXT', text: 'Bonjour depuis smsmode RCS !' },
188
+ });
189
+ ```
190
+
191
+ **Avec un agent RCS personnalisé.**
192
+ Le champ `from` permet de sélectionner l'agent parmi la liste de votre Channel (1-40 caractères, défaut : `"Default RCS Agent"`).
193
+
194
+ ```typescript
195
+ await client.send({
196
+ recipient: { to: '33600000001' },
197
+ body: { type: 'TEXT', text: 'Votre commande a été expédiée.' },
198
+ from: 'MonBrand',
199
+ });
200
+ ```
201
+
202
+ **Message programmé.**
203
+ Renseignez `sentDate` pour programmer l'envoi. Le message peut être modifié ou annulé avant l'envoi via `update()` ou `cancel()`.
204
+
205
+ ```typescript
206
+ await client.send({
207
+ recipient: { to: '33600000001' },
208
+ body: { type: 'TEXT', text: 'Rappel : votre rendez-vous est demain à 10h.' },
209
+ sentDate: '2026-06-01T10:00:00Z',
210
+ });
211
+ ```
212
+
213
+ **Avec validité personnalisée.**
214
+ Si le message ne peut pas être livré dans ce délai, il expire avec le statut `UNDELIVERED`.
215
+
216
+ ```typescript
217
+ await client.send({
218
+ recipient: { to: '33600000001' },
219
+ body: { type: 'TEXT', text: 'Votre code : 123456' },
220
+ validity: { amount: 30, timeUnit: 'SECONDS' }, // min 30s, max 48h (défaut)
221
+ });
222
+ ```
223
+
224
+ **Avec référence client et callbacks.**
225
+ `callbackUrlStatus` reçoit les accusés de réception (DLR), `callbackUrlMo` reçoit les réponses entrantes.
226
+
227
+ ```typescript
228
+ await client.send({
229
+ recipient: { to: '33600000001' },
230
+ body: { type: 'TEXT', text: 'Votre message' },
231
+ refClient: 'commande-42',
232
+ callbackUrlStatus: 'https://votre-serveur.com/webhook/dlr',
233
+ callbackUrlMo: 'https://votre-serveur.com/webhook/mo',
234
+ });
235
+ ```
236
+
237
+ **Envoi en batch.**
238
+ Jusqu'à 1 000 messages en un seul appel. Chaque message est indépendant. Les overloads TypeScript infèrent automatiquement `RcsMessage` pour un envoi unitaire et `RcsMessage[]` pour un batch.
239
+
240
+ ```typescript
241
+ const results = await client.send([
242
+ { recipient: { to: '33600000001' }, body: { type: 'TEXT', text: 'Bonjour Alice !' } },
243
+ { recipient: { to: '33600000002' }, body: { type: 'TEXT', text: 'Bonjour Bob !' } },
244
+ ]);
245
+ ```
246
+
247
+ > Pour envoyer à plus de 1 000 destinataires, utilisez les Campagnes ou découpez en plusieurs batchs.
248
+
249
+ ### Lister les messages
250
+
251
+ ```typescript
252
+ // Retourne la première page avec les paramètres par défaut
253
+ const result = await client.list();
254
+
255
+ console.log(result.totalCount); // nombre total de messages (toutes pages)
256
+ console.log(result.count); // nombre de messages dans cette page
257
+ console.log(result.items); // tableau des messages de la page courante
258
+
259
+ // Avec pagination et filtres
260
+ const filtered = await client.list({
261
+ page: 1,
262
+ pageSize: 20,
263
+ searchBy: {
264
+ direction: 'MT', // MT = envoyé, MO = reçu
265
+ to: '33600000001', // filtrer par destinataire
266
+ },
267
+ sortBy: { sentDate: 'DESC' }, // plus récents en premier
268
+ });
269
+ ```
270
+
271
+ ### Obtenir un message
272
+
273
+ ```typescript
274
+ const message = await client.get('67c15045-1067-4588-ba3c-737cc5051438');
275
+
276
+ ```
277
+
278
+ ### Modifier un message programmé
279
+
280
+ Un message programmé peut être modifié tant qu'il n'a pas encore été envoyé (et au moins 5 minutes avant l'heure d'envoi). Seuls les champs passés dans le payload sont mis à jour, les autres restent inchangés.
281
+
282
+ ```typescript
283
+ await client.update('67c15045-1067-4588-ba3c-737cc5051438', {
284
+ sentDate: '2026-06-02T09:00:00Z',
285
+ refClient: 'commande-99',
286
+ });
287
+ ```
288
+
289
+ **Effacer un champ optionnel.**
290
+ Passer une chaîne vide pour retirer un paramètre.
291
+
292
+ ```typescript
293
+ await client.update('67c15045-1067-4588-ba3c-737cc5051438', {
294
+ refClient: '',
295
+ });
296
+ ```
297
+
298
+ ### Annuler un message programmé
299
+
300
+ > **Important :** `cancel()` ne fonctionne que sur les messages **programmés** (statut `SCHEDULED`). Un message déjà parti ne peut pas être annulé.
301
+
302
+ ```typescript
303
+ await client.cancel('67c15045-1067-4588-ba3c-737cc5051438');
304
+ ```
305
+
306
+ ### Utiliser un Channel ou une Campaign spécifique
307
+
308
+ Par défaut, les appels utilisent le Channel lié à votre clé API. Vous pouvez cibler un Channel ou une Campaign spécifique via le second paramètre `options`, disponible sur toutes les méthodes.
309
+
310
+ ```typescript
311
+ // Envoyer via un Channel spécifique (nécessite les permissions Manager ou Administrator)
312
+ await client.send(
313
+ { recipient: { to: '33600000001' }, body: { type: 'TEXT', text: 'Hello !' } },
314
+ { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' }
315
+ );
316
+
317
+ // Associer le message à une Campaign existante (pour les statistiques)
318
+ await client.send(
319
+ { recipient: { to: '33600000001' }, body: { type: 'TEXT', text: 'Hello !' } },
320
+ { campaignId: '4c9f9589-1ffd-48da-82d2-65aa9e5f5f70' }
321
+ );
322
+
323
+ // Combiner les deux
324
+ await client.send(
325
+ { recipient: { to: '33600000001' }, body: { type: 'TEXT', text: 'Hello !' } },
326
+ {
327
+ channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd',
328
+ campaignId: '4c9f9589-1ffd-48da-82d2-65aa9e5f5f70',
329
+ }
330
+ );
331
+
332
+ // Le second paramètre options fonctionne sur toutes les méthodes
333
+ await client.list({ page: 1 }, { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' });
334
+ await client.get('message-id', { campaignId: '4c9f9589-1ffd-48da-82d2-65aa9e5f5f70' });
335
+ await client.update('message-id', { sentDate: '2026-06-02T09:00:00Z' }, { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' });
336
+ await client.cancel('message-id', { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' });
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Campagnes RCS
342
+
343
+ Une **Campaign** est un envoi groupé à plusieurs destinataires, traité comme une unité cohérente avec des statistiques consolidées. C'est la ressource recommandée pour les communications marketing ou les notifications de masse.
344
+
345
+ **Différence avec le batch de Messages :**
346
+ - Le batch (`client.send([...])`) envoie des messages indépendants sans lien entre eux.
347
+ - La Campaign regroupe tous les envois sous un identifiant commun (`campaignId`), ce qui permet de suivre les performances globales.
348
+
349
+ **Limite :** 1 000 destinataires par campagne. Pour dépasser cette limite, créez la campagne d'abord, puis envoyez des messages supplémentaires via `client.send([...], { campaignId })`.
350
+
351
+ ### Envoyer une campagne
352
+
353
+ Envoi simple — le minimum requis.
354
+
355
+ ```typescript
356
+ const campaign = await client.campaigns.send({
357
+ name: 'Soldes de printemps 2026',
358
+ recipients: [
359
+ { to: '33600000001' },
360
+ { to: '33600000002' },
361
+ ],
362
+ body: { type: 'TEXT', text: 'Profitez de nos offres exclusives ce week-end !' },
363
+ });
364
+
365
+ console.log(campaign.campaignId); // identifiant de la campagne
366
+ console.log(campaign.status); // "SCHEDULED" | "ONGOING" | "ENDED" | "CANCELED"
367
+ console.log(campaign.quantity); // nombre de destinataires
368
+ ```
369
+
370
+ **Avec contenu riche.**
371
+ Le champ `body` accepte tous les types RCS : BASIC, TEXT, FILE, CARD, CAROUSEL.
372
+
373
+ ```typescript
374
+ await client.campaigns.send({
375
+ name: 'Catalogue printemps',
376
+ recipients: [{ to: '33600000001' }, { to: '33600000002' }],
377
+ body: {
378
+ type: 'CAROUSEL',
379
+ cardWidth: 'MEDIUM',
380
+ contents: [
381
+ {
382
+ title: 'Nouveauté A',
383
+ media: { fileUrl: 'https://example.com/a.jpg' },
384
+ suggestions: [{ type: 'REPLY', text: 'En savoir plus', postbackData: 'plus_a' }],
385
+ },
386
+ {
387
+ title: 'Nouveauté B',
388
+ media: { fileUrl: 'https://example.com/b.jpg' },
389
+ suggestions: [{ type: 'REPLY', text: 'En savoir plus', postbackData: 'plus_b' }],
390
+ },
391
+ ],
392
+ },
393
+ });
394
+ ```
395
+
396
+ **Campagne programmée.**
397
+ Renseignez `sentDate` pour programmer l'envoi (au minimum 30 minutes dans le futur).
398
+
399
+ ```typescript
400
+ await client.campaigns.send({
401
+ name: 'Rappel rendez-vous',
402
+ recipients: [{ to: '33600000001' }],
403
+ body: { type: 'TEXT', text: 'Rappel : votre rendez-vous est demain.' },
404
+ sentDate: '2026-04-20T09:00:00Z',
405
+ });
406
+ ```
407
+
408
+ **Via un Channel spécifique.**
409
+ Nécessite les permissions Manager ou Administrator.
410
+
411
+ ```typescript
412
+ await client.campaigns.send(
413
+ { name: 'Promo', recipients: [{ to: '33600000001' }], body: { type: 'TEXT', text: 'Hello !' } },
414
+ { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' }
415
+ );
416
+ ```
417
+
418
+ ### Lister les campagnes
419
+
420
+ Retourne la première page avec les paramètres par défaut.
421
+
422
+ ```typescript
423
+ const result = await client.campaigns.list();
424
+
425
+ console.log(result.totalCount); // nombre total de campagnes
426
+ console.log(result.items); // campagnes de la page courante
427
+ ```
428
+
429
+ **Avec filtres.**
430
+
431
+ ```typescript
432
+ const scheduled = await client.campaigns.list({
433
+ searchBy: { status: 'SCHEDULED' }, // "SCHEDULED" | "ONGOING" | "ENDED" | "CANCELED"
434
+ sortBy: { sentDate: 'DESC' },
435
+ });
436
+ ```
437
+
438
+ ### Obtenir une campagne
439
+
440
+ ```typescript
441
+ const campaign = await client.campaigns.get('67c15045-1067-4588-ba3c-737cc5051438');
442
+
443
+ console.log(campaign.status); // "SCHEDULED" | "ONGOING" | "ENDED" | "CANCELED"
444
+ console.log(campaign.quantity); // nombre total de destinataires
445
+ console.log(campaign.statuses); // répartition des statuts de livraison
446
+ ```
447
+
448
+ ### Modifier une campagne programmée
449
+
450
+ Les modifications s'appliquent à tous les messages unitaires de la campagne.
451
+
452
+ ```typescript
453
+ await client.campaigns.update('67c15045-1067-4588-ba3c-737cc5051438', {
454
+ name: 'Soldes printemps v2',
455
+ sentDate: '2026-04-21T10:00:00Z',
456
+ });
457
+ ```
458
+
459
+
460
+ ### Annuler une campagne programmée
461
+
462
+ > **Important :** `update()` et `cancel()` ne fonctionnent que sur les campagnes **programmées** (statut `SCHEDULED`). Une campagne déjà en cours ou terminée ne peut pas être modifiée ni annulée.
463
+
464
+ ```typescript
465
+ await client.campaigns.cancel('67c15045-1067-4588-ba3c-737cc5051438');
466
+ ```
467
+
468
+ ---
469
+
470
+ ## Contenu riche
471
+
472
+ RCS supporte cinq types de contenu, tous typés via l'union discriminée `RcsBody`. Le champ `type` détermine la structure exacte du corps : TypeScript vous guide à chaque étape grâce à l'autocomplétion.
473
+
474
+ ### BASIC : texte brut (max 160 caractères, sans suggestions)
475
+
476
+ Idéal pour les notifications transactionnelles simples.
477
+
478
+ ```typescript
479
+ body: { type: 'BASIC', text: 'Votre colis a été livré.' }
480
+ ```
481
+
482
+ ### TEXT : texte enrichi avec suggestions (max 3 072 caractères, jusqu'à 11 suggestions)
483
+
484
+ Permet d'ajouter des boutons d'action sous le message.
485
+
486
+ ```typescript
487
+ body: {
488
+ type: 'TEXT',
489
+ text: 'Choisissez un créneau pour votre livraison :',
490
+ suggestions: [
491
+ { type: 'REPLY', text: 'Matin', postbackData: 'creneau_matin' },
492
+ { type: 'REPLY', text: 'Après-midi', postbackData: 'creneau_aprem' },
493
+ { type: 'OPEN_URL', text: 'Voir le suivi', postbackData: 'suivi', url: 'https://example.com/suivi' },
494
+ ],
495
+ }
496
+ ```
497
+
498
+ ### FILE : image, vidéo, audio ou PDF
499
+
500
+ ```typescript
501
+ // Image
502
+ body: {
503
+ type: 'FILE',
504
+ fileUrl: 'https://example.com/banniere.jpg',
505
+ }
506
+
507
+ // Vidéo avec miniature
508
+ body: {
509
+ type: 'FILE',
510
+ fileUrl: 'https://example.com/presentation.mp4',
511
+ thumbnailUrl: 'https://example.com/miniature.jpg',
512
+ }
513
+ ```
514
+
515
+ ### CARD : carte enrichie avec média, titre et suggestions
516
+
517
+ ```typescript
518
+ body: {
519
+ type: 'CARD',
520
+ orientation: 'VERTICAL', // ou "HORIZONTAL"
521
+ content: {
522
+ title: 'Soldes de printemps',
523
+ description: 'Jusqu\'à -50% ce week-end seulement.',
524
+ media: { fileUrl: 'https://example.com/banniere.jpg', height: 'MEDIUM' },
525
+ suggestions: [
526
+ { type: 'OPEN_URL', text: 'Voir les offres', postbackData: 'voir', url: 'https://example.com/soldes' },
527
+ { type: 'REPLY', text: 'Me rappeler', postbackData: 'rappel' },
528
+ ],
529
+ },
530
+ suggestions: [
531
+ { type: 'REPLY', text: 'Non merci', postbackData: 'non' },
532
+ ],
533
+ }
534
+ ```
535
+
536
+ ### CAROUSEL : de 2 à 11 cartes défilantes
537
+
538
+ Idéal pour présenter plusieurs produits ou offres dans un seul message.
539
+
540
+ ```typescript
541
+ body: {
542
+ type: 'CAROUSEL',
543
+ cardWidth: 'MEDIUM', // ou "SMALL"
544
+ contents: [
545
+ {
546
+ title: 'Produit A',
547
+ description: '29,99 €',
548
+ media: { fileUrl: 'https://example.com/produit-a.jpg' },
549
+ suggestions: [
550
+ { type: 'REPLY', text: 'Ajouter au panier', postbackData: 'ajout_a' },
551
+ ],
552
+ },
553
+ {
554
+ title: 'Produit B',
555
+ description: '49,99 €',
556
+ media: { fileUrl: 'https://example.com/produit-b.jpg' },
557
+ suggestions: [
558
+ { type: 'REPLY', text: 'Ajouter au panier', postbackData: 'ajout_b' },
559
+ ],
560
+ },
561
+ ],
562
+ }
563
+ ```
564
+
565
+ ### Types de suggestions
566
+
567
+ Les suggestions peuvent être placées à deux niveaux :
568
+
569
+ - **Dans le contenu d'une carte** (`content.suggestions` pour `CARD`, `contents[].suggestions` pour chaque carte d'un `CAROUSEL`) : max 4 suggestions. Ces boutons sont attachés directement à la carte.
570
+ - **Sous l'ensemble du message** (`body.suggestions` pour `TEXT`, `CARD` et `CAROUSEL`) : max 11 suggestions. Ces boutons apparaissent en dessous du contenu principal.
571
+
572
+ Tous les types de suggestions partagent trois champs de base : `type` (identifiant du type), `text` (texte affiché sur le bouton, max 25 caractères) et `postbackData` (payload renvoyé à l'agent quand l'utilisateur appuie, max 2048 caractères, encodé en base64). La colonne "Champs supplémentaires" liste uniquement les champs propres à chaque type.
573
+
574
+ | Type | Description | Champs supplémentaires |
575
+ |------|-------------|------------------------|
576
+ | `REPLY` | Réponse rapide prédéfinie | aucun |
577
+ | `OPEN_URL` | Ouvre une URL | `url`, `webviewSize?` |
578
+ | `DIAL_PHONE` | Lance un appel | `phoneNumber` |
579
+ | `SHOW_LOCATION` | Affiche une position sur la carte | `latitude`, `longitude`, `label?` |
580
+ | `REQUEST_LOCATION` | Demande la position du destinataire | aucun |
581
+ | `CREATE_CALENDAR_EVENT` | Crée un événement calendrier | `startTime`, `endTime`, `title`, `description?` |
582
+
583
+ **REPLY**
584
+
585
+ ```typescript
586
+ { type: 'REPLY', text: 'Confirmer', postbackData: 'confirm' }
587
+ ```
588
+
589
+ **OPEN_URL**
590
+
591
+ ```typescript
592
+ { type: 'OPEN_URL', text: 'Voir les offres', postbackData: 'offres', url: 'https://example.com/offres' }
593
+ ```
594
+
595
+ **DIAL_PHONE**
596
+
597
+ ```typescript
598
+ { type: 'DIAL_PHONE', text: 'Appeler le support', postbackData: 'support', phoneNumber: '33800000000' }
599
+ ```
600
+
601
+ **SHOW_LOCATION**
602
+
603
+ ```typescript
604
+ { type: 'SHOW_LOCATION', text: 'Notre magasin', postbackData: 'magasin', latitude: 48.8566, longitude: 2.3522 }
605
+ ```
606
+
607
+ **REQUEST_LOCATION**
608
+
609
+ ```typescript
610
+ { type: 'REQUEST_LOCATION', text: 'Partager ma position', postbackData: 'location' }
611
+ ```
612
+
613
+ **CREATE_CALENDAR_EVENT**
614
+
615
+ ```typescript
616
+ {
617
+ type: 'CREATE_CALENDAR_EVENT',
618
+ text: 'Ajouter à mon agenda',
619
+ postbackData: 'agenda',
620
+ title: 'Rendez-vous smsmode',
621
+ startTime: '2026-06-01T10:00:00Z',
622
+ endTime: '2026-06-01T11:00:00Z',
623
+ }
624
+ ```
625
+
626
+ ---
627
+
628
+ ## Templates RCS
629
+
630
+ smsmode propose une bibliothèque de templates RCS prêts à l'emploi (notifications de livraison, promotions, flows de discussion...) pour vous aider à démarrer.
631
+
632
+ **[Découvrir les templates RCS smsmode →](https://www.smsmode.com/rcs-template/)**
633
+
634
+ ---
635
+
636
+ ## Webhooks
637
+
638
+ Les webhooks sont des requêtes de callback que smsmode envoie vers votre serveur pour vous informer d'événements liés à vos messages RCS.
639
+
640
+ Il existe deux types de notifications :
641
+
642
+ **DLR (Delivery Report)** : smsmode vous notifie chaque fois que le statut d'un message RCS change (envoyé, livré, lu, en erreur...). Configuré via `callbackUrlStatus`.
643
+
644
+ **MO (Mobile Originated)** : smsmode vous notifie quand un utilisateur répond à l'un de vos messages RCS. Le corps du message MO est systématiquement de type `TEXT`. Configuré via `callbackUrlMo`.
645
+
646
+ Les deux types partagent la même structure de base (ressource Message) et se distinguent par le champ `direction` : `MT` pour un DLR, `MO` pour un message entrant. Le champ `status` n'est présent que dans un DLR.
647
+
648
+ ### Configurer l'URL de réception
649
+
650
+ 1. **Global (recommandé) :** via l'interface smsmode sous **Settings > Webhooks**. C'est la façon la plus simple : vous configurez une URL une seule fois et elle s'applique à tous vos envois, sans toucher au code.
651
+ 2. **Par channel :** via l'API Channel, si vous avez plusieurs channels et souhaitez une URL différente par channel.
652
+ 3. **Par message :** champs `callbackUrlStatus` (DLR) et `callbackUrlMo` (MO) directement dans le `send()`, pour surcharger ponctuellement la configuration globale.
653
+
654
+ ### Mécanisme de retry
655
+
656
+ Si votre endpoint ne répond pas avec un statut 2xx, smsmode retente la notification jusqu'à 6 fois :
657
+
658
+ | Tentative | Délai depuis la précédente |
659
+ |-----------|---------------------------|
660
+ | 1 | 30 secondes |
661
+ | 2 | 2 minutes |
662
+ | 3 | 10 minutes |
663
+ | 4 | 1 heure |
664
+ | 5 | 5 heures |
665
+ | 6 | 24 heures |
666
+
667
+ Après la 6ème tentative, la notification est abandonnée. Votre endpoint doit gérer les doublons en dédupliquant sur `messageId`.
668
+
669
+ ### Recevoir et identifier un webhook
670
+
671
+ Le SDK expose trois utilitaires pour traiter les webhooks de façon typée :
672
+
673
+ - `parseWebhookPayload(body)` : valide que le payload est bien formé et retourne un type discriminé
674
+ - `isDeliveryReport(payload)` : type guard : retourne `true` si `direction === 'MT'`
675
+ - `isIncomingMessage(payload)` : type guard : retourne `true` si `direction === 'MO'`
676
+
677
+ ```typescript
678
+ import express from 'express';
679
+ import {
680
+ parseWebhookPayload,
681
+ isDeliveryReport,
682
+ isIncomingMessage,
683
+ } from '@smsmode/rcs';
684
+
685
+ const app = express();
686
+ app.use(express.json());
687
+
688
+ app.post('/webhook/rcs', (req, res) => {
689
+ try {
690
+ const payload = parseWebhookPayload(req.body);
691
+
692
+ if (isDeliveryReport(payload)) {
693
+ // Rapport de livraison (DLR) : direction: 'MT'
694
+ // payload.status.value : "ENROUTE", "DELIVERED", "READ", "UNDELIVERABLE"...
695
+ console.log(`Message ${payload.messageId} : statut : ${payload.status.value}`);
696
+
697
+ } else if (isIncomingMessage(payload)) {
698
+ // Message entrant (MO) : direction: 'MO'
699
+ // payload.body est toujours de type RcsTextBody
700
+ console.log(`Réponse de ${payload.recipient.to} : ${payload.body.text}`);
701
+
702
+ // originMessageId identifie le message MT auquel ce MO répond
703
+ console.log(`En réponse au message : ${payload.originMessageId}`);
704
+ }
705
+
706
+ // Toujours répondre avec un statut 2xx pour acquitter la notification
707
+ // Sans cela, smsmode considérera la notification comme échouée et retentera
708
+ res.sendStatus(200);
709
+
710
+ } catch {
711
+ // parseWebhookPayload lève une ValidationError si le payload est invalide
712
+ res.sendStatus(400);
713
+ }
714
+ });
715
+ ```
716
+
717
+ ---
718
+
719
+ ## Patterns
720
+
721
+ ### Pagination complète
722
+
723
+ La réponse paginée contient `totalCount` (total global) et `items` (page courante). Pour parcourir toutes les pages :
724
+
725
+ ```typescript
726
+ let page = 1;
727
+ let hasMore = true;
728
+ const allMessages: RcsMessage[] = [];
729
+
730
+ while (hasMore) {
731
+ const result = await client.list({ page, pageSize: 100 });
732
+
733
+ allMessages.push(...result.items);
734
+
735
+ // S'il y a moins d'items que la taille de page, on est sur la dernière page
736
+ hasMore = result.items.length === result.pageSize;
737
+ page++;
738
+ }
739
+
740
+ console.log(`${allMessages.length} messages récupérés`);
741
+ ```
742
+
743
+ ### Gestion du rate limit
744
+
745
+ Le SDK lève une `RateLimitError` sur les réponses HTTP 429. L'erreur expose `retryAfter`, la durée d'attente recommandée en secondes fournie par l'API. Vous pouvez implémenter un retry automatique selon votre besoin :
746
+
747
+ ```typescript
748
+ import { RcsSendPayload, RateLimitError } from '@smsmode/rcs';
749
+
750
+ async function sendWithRetry(payload: RcsSendPayload, maxRetries = 3) {
751
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
752
+ try {
753
+ return await client.send(payload);
754
+ } catch (error) {
755
+ if (error instanceof RateLimitError && attempt < maxRetries - 1) {
756
+ const waitMs = (error.retryAfter ?? 60) * 1000;
757
+ await new Promise(resolve => setTimeout(resolve, waitMs));
758
+ continue;
759
+ }
760
+ throw error; // autres erreurs ou dernière tentative : on laisse remonter
761
+ }
762
+ }
763
+ }
764
+ ```
765
+
766
+ ---
767
+
768
+ ## Gestion des erreurs
769
+
770
+ > **Crédit insuffisant :** `client.send()` ne lève pas d'erreur quand le crédit est insuffisant. L'API répond `200` avec le statut `ENROUTE`, puis vous envoie un DLR `UNDELIVERABLE`. Si vous ne traitez pas vos DLR, vous ne le saurez jamais.
771
+
772
+ Le SDK transforme chaque réponse d'erreur HTTP en une classe TypeScript typée. La distinction centrale est la présence ou non d'un body d'erreur structuré smsmode dans la réponse, pas le code HTTP lui-même. Cela détermine ce que vous pouvez réellement faire avec l'erreur.
773
+
774
+ Hiérarchie des classes d'erreur :
775
+
776
+ ```
777
+ Error
778
+ └── RcsError
779
+ ├── SmsModeApiError : body smsmode structuré présent
780
+ │ ├── AuthError : HTTP 401
781
+ │ └── RateLimitError : HTTP 429 (+ retryAfter)
782
+ └── SmsModeHttpError : pas de body structuré (5xx, gateway timeout, HTML d'un reverse proxy)
783
+ ```
784
+
785
+ ```typescript
786
+ import {
787
+ SmsmodeRcsClient,
788
+ SmsModeApiError,
789
+ SmsModeHttpError,
790
+ AuthError,
791
+ RateLimitError,
792
+ } from '@smsmode/rcs';
793
+
794
+ const client = new SmsmodeRcsClient({ apiKey: process.env.SMSMODE_API_KEY });
795
+
796
+ try {
797
+
798
+ await client.send({
799
+ recipient: { to: '33600000001' },
800
+ body: { type: 'TEXT', text: 'Bonjour !' },
801
+ });
802
+ console.log(message.messageId);
803
+ console.log(message.status.value);
804
+
805
+ } catch (error) {
806
+ if (error instanceof AuthError) {
807
+ // Clé API invalide : vérifier process.env.SMSMODE_API_KEY
808
+ console.error('Clé API invalide');
809
+
810
+ } else if (error instanceof RateLimitError) {
811
+ // Trop de requêtes : attendre retryAfter secondes
812
+ const wait = error.retryAfter ?? 60;
813
+ console.error(`Rate limit atteint, retry dans ${wait}s`);
814
+
815
+ } else if (error instanceof SmsModeApiError) {
816
+ console.error(" httpStatus :", error.httpStatus);
817
+ console.error(" errorCode :", error.errorCode);
818
+ console.error(" message :", error.message);
819
+ console.error(" detail :", error.detail);
820
+ console.error(" docsUrl :", error.docsUrl);
821
+
822
+ // Branchement fin possible sur error.errorCode :
823
+ switch (error.errorCode) {
824
+ case '400.029': // format E.164 invalide
825
+ // etc.
826
+ }
827
+
828
+ } else if (error instanceof SmsModeHttpError) {
829
+ // Pas de body structuré : 5xx, gateway timeout, reverse proxy
830
+ console.error(`HTTP ${error.httpStatus} ${error.statusText} : retry recommandé`);
831
+
832
+ } else {
833
+ throw error;
834
+ }
835
+ }
836
+ ```
837
+
838
+ Classes d'erreur exposées par le SDK :
839
+
840
+ | Classe | Quand ? | Propriétés |
841
+ |--------|---------|------------|
842
+ | `SmsModeApiError` | L'API smsmode a répondu avec un body d'erreur structuré (4xx principalement) | `httpStatus`, `title`, `message`, `detail`, `errorCode`, `docsUrl`, `details` |
843
+ | `AuthError` | HTTP 401, clé API invalide | Hérite de `SmsModeApiError` |
844
+ | `RateLimitError` | HTTP 429 avec body structuré | Hérite de `SmsModeApiError` + `retryAfter?: number` |
845
+ | `SmsModeHttpError` | Réponse HTTP en erreur sans body structuré (5xx, gateway timeout, HTML d'un reverse proxy) | `httpStatus`, `statusText` |
846
+
847
+ Propriétés de `SmsModeApiError` :
848
+
849
+ | Propriété | Type | Description |
850
+ |-----------|------|-------------|
851
+ | `httpStatus` | `number` | Code HTTP (ex : `400`) |
852
+ | `title` | `string` | Titre HTTP humain (ex : `"Bad Request"`) |
853
+ | `message` | `string` | Description courte de l'erreur |
854
+ | `detail` | `string` | Contrainte précise non respectée |
855
+ | `errorCode` | `string` | Code métier smsmode (ex : `"400.029"`) |
856
+ | `docsUrl` | `string` | URL vers la documentation du code d'erreur |
857
+ | `details` | `unknown` | Body brut complet de la réponse |
858
+
859
+ Tous les champs de `SmsModeApiError` sont garantis non-undefined. La présence du body structuré smsmode est vérifiée avant de lancer cette classe.
860
+
861
+ Pour la liste exhaustive des codes d'erreur métier smsmode, consultez la [documentation officielle des codes de statut](https://dev.smsmode.com/sms/status-codes/).
862
+
863
+ ---
864
+
865
+ ## Référence API
866
+
867
+ ### `SmsmodeRcsClient`
868
+
869
+ | Option | Type | Défaut | Description |
870
+ |--------|------|--------|-------------|
871
+ | `apiKey` | `string` | — | **Obligatoire.** Clé API smsmode |
872
+ | `timeout` | `number` | `10000` | Timeout en millisecondes |
873
+
874
+ ### `client` : Messages
875
+
876
+ | Méthode | Paramètres | Retour | Description |
877
+ |---------|------------|--------|-------------|
878
+ | `send(payload, options?)` | `RcsSendPayload \| RcsSendPayload[]` | `RcsMessage \| RcsMessage[]` | Envoyer un message unitaire ou batch (max 1 000) |
879
+ | `list(params?, options?)` | `RcsListParams` | `PaginatedResponse<RcsMessage>` | Lister les messages avec filtres et pagination |
880
+ | `get(messageId, options?)` | `string` | `RcsMessage` | Obtenir un message par son ID |
881
+ | `update(messageId, payload, options?)` | `string, RcsUpdatePayload` | `RcsMessage` | Modifier un message programmé |
882
+ | `cancel(messageId, options?)` | `string` | `void` | Annuler un message programmé |
883
+
884
+ ### `client.campaigns` : Campagnes
885
+
886
+ | Méthode | Paramètres | Retour | Description |
887
+ |---------|------------|--------|-------------|
888
+ | `send(payload, options?)` | `RcsCampaignSendPayload` | `RcsCampaign` | Envoyer ou programmer une campagne (max 1 000 destinataires) |
889
+ | `list(params?, options?)` | `RcsCampaignListParams` | `PaginatedResponse<RcsCampaign>` | Lister les campagnes avec filtres et pagination |
890
+ | `get(campaignId, options?)` | `string` | `RcsCampaign` | Obtenir une campagne par son ID |
891
+ | `update(campaignId, payload, options?)` | `string, RcsCampaignUpdatePayload` | `RcsCampaign` | Modifier une campagne programmée |
892
+ | `cancel(campaignId, options?)` | `string` | `void` | Annuler une campagne programmée |
893
+
894
+ ### Webhooks
895
+
896
+ | Fonction | Paramètres | Retour | Description |
897
+ |----------|------------|--------|-------------|
898
+ | `parseWebhookPayload(body)` | `unknown` | `RcsWebhookPayload` | Valide et type-asserte un payload entrant. Lève une `ValidationError` si invalide. |
899
+ | `isDeliveryReport(payload)` | `RcsWebhookPayload` | `boolean` | Type guard : `true` si DLR (`direction: 'MT'`) |
900
+ | `isIncomingMessage(payload)` | `RcsWebhookPayload` | `boolean` | Type guard : `true` si MO (`direction: 'MO'`) |
901
+
902
+ ---
903
+
904
+
905
+ ## Ressources
906
+
907
+ - [Documentation API smsmode](https://dev.smsmode.com) : référence complète de l'API REST
908
+ - [Templates RCS smsmode](https://www.smsmode.com/rcs-template/) : bibliothèque de templates et flows de discussion
909
+ - [Collection Postman](https://www.postman.com/smsmode/smsmode-public) : pour tester les endpoints manuellement
910
+ - [Espace client smsmode](https://ui.smsmode.com) : gestion des clés API, crédits, webhooks
911
+ - [Support](https://support.smsmode.com)
912
+
913
+ ---
914
+
915
+ ## Changelog
916
+
917
+ Voir [CHANGELOG.md](./CHANGELOG.md) pour l'historique des versions.
918
+
919
+ ---
920
+
921
+ ## Licence
922
+
923
+ MIT — voir [LICENSE](./LICENSE)