{"openapi":"3.1.0","info":{"title":"GESFLOW SENDER API","version":"1.0.0","description":"API REST de **GESFLOW SENDER** pour envoyer des SMS (passerelle opérateur) et des messages WhatsApp (via le numéro WhatsApp appairé à votre compte), envoyer et vérifier des codes OTP, suivre vos campagnes, lire les messages reçus et recharger votre solde par Mobile Money.\n\n## Authentification\n\nToutes les routes, sauf `GET /api/v1/payments/fees-config`, exigent une **clé API** transmise dans l'en-tête :\n\n```\nAuthorization: Bearer VOTRE_CLE_API\n```\n\n- Créez la clé depuis l'application web, menu **Compte → Clés API**. La clé en clair (format `<id>|<secret>`) n'est affichée **qu'une seule fois**, juste après sa création.\n- Une date d'expiration est obligatoire à la création : la clé reste valable jusqu'à la fin de la journée choisie. Une clé peut être révoquée à tout moment depuis la même page.\n- Deux types de clés existent : **production** (envois réels et facturés) et **test / sandbox** (voir plus bas).\n- La clé est rattachée à l'utilisateur qui l'a créée, mais donne accès aux données de **tout le compte (entreprise)** : messages, campagnes, paiements, Sender IDs… de tous les membres.\n- Clé absente, invalide, expirée ou révoquée : réponse `401` avec `{\"message\": \"Unauthenticated.\"}`.\n\n## URL de base et format\n\n- Tous les chemins sont préfixés par `/api/v1` (ex. `https://sender.gesflow.biz/api/v1/messages`).\n- Les corps de requête sont acceptés en JSON (`Content-Type: application/json`) ; `POST /api/v1/messages/bulk` accepte aussi `multipart/form-data` pour l'envoi d'un fichier de contacts. Il est recommandé d'envoyer `Accept: application/json`, même si toutes les erreurs des routes `/api/*` sont de toute façon rendues en JSON.\n- Les dates sont renvoyées en ISO 8601, en UTC (ex. `2026-10-07T09:15:00.000000Z`). Les dates envoyées sans fuseau horaire sont interprétées en UTC.\n- Les montants monétaires des messages et des paiements sont renvoyés sous forme de **chaînes décimales à 4 décimales** (ex. `\"30.0000\"`), en FCFA (XAF).\n\n## Format des numéros de téléphone\n\n- Format international **sans le signe +**, uniquement des chiffres, de **8 à 15 chiffres** (ex. `242066000000`).\n- Pour `POST /messages`, `POST /messages/bulk` (`phones`), `POST /verify/send`, `POST /verify/check` et `GET /whatsapp/check-number`, tous les caractères non numériques sont supprimés avant validation : `+242 06 600 00 00` devient `242066000000`.\n- Le filtre `phone` de `GET /inbound-messages` n'est **pas** nettoyé : il doit déjà contenir uniquement 8 à 15 chiffres.\n- Exception : le champ `phone_number` de `POST /payments/initiate` est normalisé au format international **avec** `+` (préfixe `+242` ajouté s'il est absent).\n\n## Canaux\n\n| Valeur | Description |\n|---|---|\n| `sms` | Canal par défaut. Envoi via la passerelle SMS, avec l'un des Sender IDs actifs de votre compte comme expéditeur. |\n| `whatsapp` | Envoi depuis le numéro WhatsApp appairé au compte (session WhatsApp connectée requise, sauf en sandbox). Le champ `from` vaut toujours `WhatsApp`. Médias, position et fiches contact sont possibles. |\n\nL'envoi en masse (`POST /messages/bulk`) accepte les deux canaux : jusqu'à **10 000 destinataires** en SMS et **500** sur WhatsApp (texte et/ou média ; pas de position ni de fiche contact).\n\n## Mode sandbox (clé de test)\n\nLors de la création d'une clé, choisissez le mode « Test (sandbox) ». Avec une telle clé :\n\n- `POST /messages`, `POST /messages/bulk` et `POST /verify/send` **simulent** l'envoi : rien n'est transmis aux opérateurs et **rien n'est débité** (`price` = `\"0.0000\"`, champ `sandbox: true`).\n- La remise est simulée immédiatement : la réponse de création renvoie déjà le statut `sent` (même si `scheduled_at` est fourni). Le numéro **`242000000000`** simule toujours un échec (statut `failed`).\n- Les webhooks de statut (`callback_url`) sont tout de même envoyés.\n- Les règles de validation s'appliquent normalement (Sender ID, format…) ; en revanche aucune session WhatsApp n'est requise et le solde n'est pas contrôlé.\n- `POST /verify/send` renvoie le code généré dans le champ `code`. Un code sandbox ne peut être vérifié qu'avec une clé sandbox (et inversement).\n- `GET /messages` et `GET /messages/{message}` ne voient que les messages sandbox ; une clé de production ne voit jamais les messages sandbox.\n- Aucune campagne n'est créée pour un envoi en masse sandbox (`meta.campaign_id` vaut `null`).\n- ⚠️ Les autres routes ne font **pas** de distinction : une clé sandbox liste les campagnes, messages entrants et paiements réels du compte, lit et modifie ses vrais contacts, groupes et modèles (mais ne peut pas annuler une campagne, toujours réelle), `GET /whatsapp/check-number` interroge réellement WhatsApp (session connectée requise), et `POST /payments/initiate` déclenche un **vrai** paiement Mobile Money.\n\n## Crédits et tarification\n\n- Chaque message est payé sur le **solde de crédits** du compte (en FCFA). Le prix unitaire d'un SMS est le prix spécifique de votre compte s'il existe, sinon le tarif global de la plateforme ; le prix d'un message WhatsApp est un tarif fixe global, quelle que soit la longueur.\n- Le prix est **débité à la création** du message (y compris pour un envoi programmé). Si le solde est insuffisant, la requête échoue en `422` (`Solde de crédits insuffisant pour envoyer ce message.`) et rien n'est créé. Un envoi en masse est débité en une seule fois : soit tous les messages sont créés, soit aucun.\n- SMS : une fois le message accepté par la passerelle, son prix est ajusté à *prix unitaire × nombre de segments SMS* facturés par la passerelle ; la différence est débitée (sans nouveau contrôle de solde) et le champ `price` est mis à jour.\n- **Remboursement automatique** du prix du message lorsque : l'envoi échoue définitivement après les tentatives automatiques ; l'opérateur signale un échec non facturable ; WhatsApp refuse le message (numéro sans compte WhatsApp, restriction temporaire du numéro, accusé d'erreur) ; un envoi programmé est annulé (application web, `POST /messages/{message}/cancel` ou `POST /campaigns/{campaign}/cancel`).\n- **Pas de remboursement** lorsque l'opérateur signale un échec facturable (abonné absent `ABS`, erreur de remise `ERD`, pas d'accusé opérateur `NRC`, abonné inconnu `USB`) : le message passe en `failed` mais reste facturé.\n- Le solde se recharge par Mobile Money (`POST /payments/initiate`) : seul le **montant de base** (`base_amount`) est crédité, les frais et taxes s'y ajoutent à la charge du payeur.\n- Chaque mouvement du solde (envoi, envoi en masse, ajustement de SMS long, remboursement, annulation, recharge) est enregistré avec le solde qu'il laisse : consultez le solde et vos tarifs avec `GET /account`, l'historique avec `GET /account/transactions`.\n\n## Variables de personnalisation\n\nLe texte des messages (`POST /messages`, `POST /messages/bulk`) peut contenir des variables `{{ variable }}` ou `{{ variable|valeur par défaut }}`, remplacées à partir de vos **contacts** enregistrés pour le numéro destinataire : `phone` / `telephone` (toujours disponibles, même sans contact enregistré), `name` / `nom` / `contact`, `prenom` / `first_name` et chaque champ personnalisé du contact (ex. `{{ ville }}`). Une variable inconnue ou vide est remplacée par sa valeur par défaut, ou par une chaîne vide.\n\n## Erreurs\n\nToutes les erreurs sont des objets JSON contenant au minimum un champ `message`.\n\n| Code | Signification |\n|---|---|\n| `400` | En-tête `Idempotency-Key` vide ou de plus de 255 caractères. |\n| `401` | Clé API absente, invalide, expirée ou révoquée. `{\"message\": \"Unauthenticated.\"}` |\n| `404` | Ressource inexistante ou appartenant à un autre compte. |\n| `409` | Une requête avec la même `Idempotency-Key` est encore en cours de traitement : réessayez après le délai de l'en-tête `Retry-After`. |\n| `422` | Erreur de validation **ou** règle métier. Validation : `{\"message\": \"...\", \"errors\": {\"champ\": [\"...\"]}}` (messages en français ; le résumé `message` peut se terminer par « (and N more errors) »). Règle métier : `{\"message\": \"...\"}` sans `errors` (solde insuffisant, Sender ID non configuré, session WhatsApp non connectée, message ou campagne plus annulable, clé d'idempotence déjà utilisée pour une autre requête). |\n| `429` | Limite de débit dépassée (voir « Limites de débit »), ou limite d'envoi de codes OTP de `POST /verify/send` : `{\"message\": \"...\", \"retry_after\": 24}` + en-tête `Retry-After`. |\n| `500` | Erreur interne (`{\"message\": \"Server Error\"}`). |\n| `503` | `GET /whatsapp/check-number` uniquement : WhatsApp injoignable. |\n\nL'API ne renvoie pas de `403` : toute clé valide a accès à l'ensemble des routes de son compte.\n\n## Limites de débit\n\nChaque compte peut effectuer **600 requêtes par minute**, toutes clés API confondues ; les routes publiques (`GET /payments/fees-config`) sont limitées à **60 requêtes par minute et par adresse IP**. Chaque réponse indique le quota dans les en-têtes `X-RateLimit-Limit` et `X-RateLimit-Remaining`. Au-delà : `429` avec l'en-tête `Retry-After` et `{\"message\": \"Trop de requêtes. Réessayez dans 24 secondes.\", \"retry_after\": 24}`. Pour envoyer beaucoup de messages, préférez `POST /messages/bulk` (un seul appel pour 10 000 destinataires).\n\nEn plus, `POST /verify/send` est limité par numéro de téléphone et par compte : **30 secondes minimum** entre deux codes et **5 codes maximum par fenêtre de 10 minutes** (compteurs séparés pour les clés sandbox). Au-delà : `429`.\n\nPour WhatsApp, un mécanisme anti-blocage côté plateforme peut **étaler** l'envoi des messages (le champ `deferred_until` indique alors l'heure d'envoi prévue) sans que la requête API ne soit refusée.\n\n## Pagination\n\nLes listes paginées suivent le format standard des collections de ressources Laravel : `data` (tableau), `links` (`first`, `last`, `prev`, `next`) et `meta` (`current_page`, `from`, `last_page`, `links`, `path`, `per_page`, `to`, `total`). Utilisez le paramètre `page` (à partir de 1).\n\n- `GET /messages`, `GET /campaigns`, `GET /inbound-messages`, `GET /account/transactions`, `GET /contacts`, `GET /contact-groups`, `GET /templates` : **25 éléments par page**, non modifiable.\n- `GET /payments/me` : 15 par défaut, modifiable via `per_page`.\n- Pour `GET /messages`, `GET /campaigns`, `GET /inbound-messages` et `GET /payments/me`, les URL de `links` et `meta.links` ne reprennent **que** le paramètre `page` : ajoutez vous-même vos filtres (`phone`, `since`, `per_page`) lorsque vous les suivez. Les autres listes conservent leurs filtres.\n\n## Webhooks\n\nLa plateforme peut appeler vos serveurs (voir la section `webhooks` de cette spécification) :\n\n- **Statut des messages** : passez un `callback_url` lors de l'envoi ; un `POST` JSON contenant le message complet est envoyé à chaque changement de statut.\n- **Messages entrants** : configurez l'URL HTTPS dans **Compte → Entreprise** ; chaque SMS ou message WhatsApp reçu est transmis sous la forme `{\"event\": \"message.received\", \"data\": {...}}`.\n\nChaque webhook est **signé** avec le secret de votre compte, affiché dans **Compte → Entreprise** (rubrique « Signature des webhooks ») :\n\n| En-tête | Contenu |\n|---|---|\n| `X-Gesflow-Event` | `message.status` ou `message.received`. |\n| `X-Gesflow-Delivery` | Identifiant unique du webhook (UUID), **identique d'une tentative à l'autre** : ignorez un identifiant déjà traité. |\n| `X-Gesflow-Timestamp` | Horodatage Unix (secondes) de l'envoi. |\n| `X-Gesflow-Signature` | `sha256=` suivi du HMAC-SHA256 hexadécimal de `{X-Gesflow-Timestamp}.{corps brut}`, calculé avec votre secret. |\n| `X-Gesflow-Previous-Signature` | Pendant les 24 heures qui suivent une régénération du secret : la même signature, calculée avec l'ancien secret. |\n\nPour vérifier un webhook : recalculez le HMAC sur le corps **brut** (avant tout décodage JSON), comparez-le en temps constant à la signature reçue et refusez les requêtes dont l'horodatage a plus de 5 minutes. Exemple en PHP :\n\n```php\n$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);\n$valid = hash_equals($expected, $signature) && abs(time() - (int) $timestamp) < 300;\n```\n\nUn propriétaire du compte peut générer un nouveau secret à tout moment : les webhooks suivants sont signés avec lui. Pendant 24 heures, ils portent aussi l'en-tête `X-Gesflow-Previous-Signature`, calculé avec l'ancien secret : acceptez l'une ou l'autre signature le temps de mettre vos serveurs à jour.\n\nLes webhooks ne sont envoyés qu'à des adresses **publiques** : une URL visant `localhost`, une adresse IP privée, réservée ou locale (ou un nom de domaine qui y mène) est refusée à l'envoi (`422`) ou, si son nom de domaine y mène plus tard, n'est pas appelée. Les redirections ne sont pas suivies. Délai de réponse maximal : 10 secondes ; seule une réponse **`2xx`** est un succès ; sinon 3 tentatives au total, espacées de 60 secondes. Après échec de toutes les tentatives, les propriétaires du compte sont prévenus par e-mail (sauf messages sandbox).\n\n## Idempotence\n\nLes requêtes `POST` acceptent l'en-tête **`Idempotency-Key`** (1 à 255 caractères : un UUID, votre référence de commande…). Une requête renvoyée avec la même clé (après un timeout réseau, par exemple) n'est **pas exécutée une seconde fois** : la réponse de la première est renvoyée telle quelle, avec l'en-tête `Idempotent-Replayed: true`, et rien n'est débité à nouveau.\n\n- Les clés sont propres à votre compte et conservées **24 heures**.\n- Seules les réponses réussies (`2xx`) sont mémorisées : après une erreur (validation, solde insuffisant…), la même clé peut être réutilisée.\n- Clé déjà utilisée pour une requête différente (autre route, autre corps, autre type de clé API) : `422`.\n- Première requête encore en cours de traitement : `409` avec `Retry-After: 1` (si elle a été interrompue côté serveur, sa clé est libérée au bout d'une heure).\n\nSans `Idempotency-Key`, chaque `POST /messages`, `POST /messages/bulk` ou `POST /verify/send` réussi crée de nouveaux messages et débite le solde. Dans un envoi en masse, les numéros en double sont dédupliqués."},"servers":[{"url":"https://sender.gesflow.biz","description":"GESFLOW SENDER"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Messages","description":"Envoi de SMS et de messages WhatsApp, unitaire ou en masse, et suivi de leur statut."},{"name":"Campagnes","description":"Campagnes créées par les envois en masse, avec leurs statistiques de remise."},{"name":"Messages entrants","description":"Réponses SMS et messages WhatsApp reçus par le compte."},{"name":"OTP (Vérification)","description":"Envoi et vérification de codes à usage unique par SMS ou WhatsApp."},{"name":"WhatsApp","description":"Fonctions propres au canal WhatsApp (session appairée au compte)."},{"name":"Sender IDs","description":"Noms d'expéditeur SMS actifs du compte."},{"name":"Paiements","description":"Recharge du solde de crédits par Mobile Money (MTN, Airtel)."},{"name":"Compte","description":"Solde de crédits, tarifs et historique des mouvements du solde."},{"name":"Contacts","description":"Carnet de contacts du compte, utilisé par les variables de personnalisation et les envois en masse."},{"name":"Groupes de contacts","description":"Groupes de contacts, ciblables lors d’un envoi en masse (`group_ids`)."},{"name":"Modèles","description":"Modèles de messages réutilisables."}],"paths":{"/api/v1/sender-ids":{"get":{"tags":["Sender IDs"],"operationId":"listSenderIds","summary":"Lister les Sender IDs","description":"Renvoie les Sender IDs **actifs** (non suspendus) du compte, utilisables dans `sender_id`. Le Sender ID par défaut apparaît en premier, puis les autres par ordre alphabétique. Liste non paginée.\n\nLes Sender IDs se demandent et se valident depuis l'application web ; un Sender ID suspendu n'apparaît plus et est refusé à l'envoi.","responses":{"200":{"description":"Liste des Sender IDs actifs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SenderIdCollection"},"examples":{"senderIds":{"summary":"Deux Sender IDs","value":{"data":[{"id":1,"value":"MaMarque","is_default":true,"created_at":"2026-01-15T10:30:00.000000Z"},{"id":2,"value":"GESFLOW","is_default":false,"created_at":"2026-03-22T14:12:00.000000Z"}]}},"empty":{"summary":"Aucun Sender ID actif","value":{"data":[]}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/messages":{"get":{"tags":["Messages"],"operationId":"listMessages","summary":"Lister les messages","description":"Liste les messages envoyés par le compte (SMS et WhatsApp, tous utilisateurs confondus), du plus récent au plus ancien (date de création), **25 par page**. Aucun filtre n'est disponible.\n\nAvec une clé sandbox, seuls les messages sandbox sont listés ; avec une clé de production, seuls les messages réels.","parameters":[{"name":"page","in":"query","required":false,"description":"Numéro de page (à partir de 1).","schema":{"type":"integer","minimum":1,"default":1},"example":1}],"responses":{"200":{"description":"Page de messages.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageCollection"},"examples":{"page":{"summary":"Première page","value":{"data":[{"id":42,"campaign_id":null,"sandbox":false,"channel":"sms","phone":"242066000000","text":"Bonjour depuis mon application","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"sent","failure_reason":null,"price":"30.0000","currency":"XAF","scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":"2026-10-07T09:15:04.000000Z","created_at":"2026-10-07T09:15:00.000000Z"},{"id":46,"campaign_id":null,"sandbox":false,"channel":"whatsapp","phone":"242066000000","text":"Notre nouvelle collection","media_url":"https://cdn.example.com/catalogue/automne.jpg","media_type":"image","interactive_type":null,"interactive_payload":null,"from":"WhatsApp","status":"sent","failure_reason":null,"price":"2.5000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":"2026-10-07T09:15:20.000000Z","created_at":"2026-10-07T09:15:00.000000Z"}],"links":{"first":"https://sender.gesflow.biz/api/v1/messages?page=1","last":"https://sender.gesflow.biz/api/v1/messages?page=5","prev":null,"next":"https://sender.gesflow.biz/api/v1/messages?page=2"},"meta":{"current_page":1,"from":1,"last_page":5,"links":[{"url":null,"label":"&laquo; Précédent","page":null,"active":false},{"url":"https://sender.gesflow.biz/api/v1/messages?page=1","label":"1","page":1,"active":true},{"url":"https://sender.gesflow.biz/api/v1/messages?page=2","label":"2","page":2,"active":false},{"url":"https://sender.gesflow.biz/api/v1/messages?page=2","label":"Suivant &raquo;","page":2,"active":false}],"path":"https://sender.gesflow.biz/api/v1/messages","per_page":25,"to":25,"total":112}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"tags":["Messages","WhatsApp"],"operationId":"sendMessage","summary":"Envoyer un message (SMS ou WhatsApp)","description":"Crée un message et le place en file d'envoi. Le prix est **débité immédiatement** du solde (sauf sandbox). La réponse `201` renvoie le message avec le statut `processing` (envoi immédiat) ou `scheduled` (envoi programmé) ; suivez ensuite son statut via `GET /messages/{message}` ou le webhook `callback_url`.\n\n**SMS** : un Sender ID actif est requis (`sender_id`, sinon Sender ID par défaut ou unique Sender ID actif).\n\n**WhatsApp** (`channel: whatsapp`) : une session WhatsApp connectée est requise (hors sandbox). Le numéro est vérifié sur WhatsApp au moment de l'envoi : s'il n'a pas de compte WhatsApp, le message passe en `failed` et est remboursé. Médias (`media_url` + `media_type`), position (`latitude`, `longitude`, `title`) ou fiches contact (`contacts`) sont possibles — un seul type par message. Les messages à boutons ou à listes ne sont **pas** pris en charge. L'anti-blocage peut étaler l'envoi (`deferred_until`).\n\n**Sandbox** : rien n'est envoyé ni débité ; la réponse porte déjà le statut final simulé (`sent`, ou `failed` pour `242000000000`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"},"examples":{"smsSimple":{"summary":"SMS simple","value":{"phone":"242066000000","text":"Bonjour depuis mon application"}},"smsComplet":{"summary":"SMS avec Sender ID, flash et webhook","value":{"channel":"sms","phone":"+242 06 600 00 00","text":"Bonjour {{ prenom|cher client }}, votre commande est prête.","sender_id":"MaMarque","flash":false,"callback_url":"https://votre-domaine.com/webhooks/sms-status"}},"smsProgramme":{"summary":"SMS programmé","value":{"phone":"242066000000","text":"Rappel : votre rendez-vous est demain à 10 h.","scheduled_at":"2026-10-08T08:00:00Z"}},"whatsappTexte":{"summary":"WhatsApp texte","value":{"channel":"whatsapp","phone":"242066000000","text":"Bonjour sur WhatsApp"}},"whatsappImage":{"summary":"WhatsApp image avec légende","value":{"channel":"whatsapp","phone":"242066000000","text":"Notre nouvelle collection","media_url":"https://cdn.example.com/catalogue/automne.jpg","media_type":"image"}},"whatsappDocument":{"summary":"WhatsApp document","value":{"channel":"whatsapp","phone":"242066000000","text":"Votre facture d'octobre","media_url":"https://cdn.example.com/factures/F-2026-10.pdf","media_type":"file"}},"whatsappVocal":{"summary":"WhatsApp message vocal (sans texte)","value":{"channel":"whatsapp","phone":"242066000000","media_url":"https://cdn.example.com/audio/bienvenue.ogg","media_type":"voice"}},"whatsappPosition":{"summary":"WhatsApp position","value":{"channel":"whatsapp","phone":"242066000000","latitude":-4.2634,"longitude":15.2429,"title":"Agence Centre-ville"}},"whatsappContact":{"summary":"WhatsApp fiche contact","value":{"channel":"whatsapp","phone":"242066000000","contacts":[{"full_name":"Service client","phone_number":"+242066000099","organization":"MaMarque"}]}},"whatsappProgramme":{"summary":"WhatsApp programmé avec webhook","value":{"channel":"whatsapp","phone":"242066000000","text":"Votre livraison arrive demain.","scheduled_at":"2026-10-08T08:00:00Z","callback_url":"https://votre-domaine.com/webhooks/wa-status"}}}}}},"responses":{"201":{"description":"Message créé (et débité, hors sandbox).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"},"examples":{"sms":{"summary":"SMS créé","value":{"data":{"id":42,"campaign_id":null,"sandbox":false,"channel":"sms","phone":"242066000000","text":"Bonjour depuis mon application","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"processing","failure_reason":null,"price":"30.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}}},"smsProgramme":{"summary":"SMS programmé","value":{"data":{"id":43,"campaign_id":null,"sandbox":false,"channel":"sms","phone":"242066000000","text":"Rappel : votre rendez-vous est demain à 10 h.","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"scheduled","failure_reason":null,"price":"30.0000","currency":null,"scheduled_at":"2026-10-08T08:00:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}}},"whatsapp":{"summary":"WhatsApp texte créé","value":{"data":{"id":45,"campaign_id":null,"sandbox":false,"channel":"whatsapp","phone":"242066000000","text":"Bonjour sur WhatsApp","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"WhatsApp","status":"processing","failure_reason":null,"price":"2.5000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}}},"whatsappImage":{"summary":"WhatsApp image créé","value":{"data":{"id":46,"campaign_id":null,"sandbox":false,"channel":"whatsapp","phone":"242066000000","text":"Notre nouvelle collection","media_url":"https://cdn.example.com/catalogue/automne.jpg","media_type":"image","interactive_type":null,"interactive_payload":null,"from":"WhatsApp","status":"processing","failure_reason":null,"price":"2.5000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}}},"whatsappPosition":{"summary":"WhatsApp position créé","value":{"data":{"id":47,"campaign_id":null,"sandbox":false,"channel":"whatsapp","phone":"242066000000","text":"","media_url":null,"media_type":null,"interactive_type":"location","interactive_payload":{"latitude":-4.2634,"longitude":15.2429,"title":"Agence Centre-ville"},"from":"WhatsApp","status":"processing","failure_reason":null,"price":"2.5000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}}},"whatsappContact":{"summary":"WhatsApp fiche contact créé","value":{"data":{"id":48,"campaign_id":null,"sandbox":false,"channel":"whatsapp","phone":"242066000000","text":"","media_url":null,"media_type":null,"interactive_type":"contact","interactive_payload":{"contacts":[{"full_name":"Service client","phone_number":"+242066000099","organization":"MaMarque"}]},"from":"WhatsApp","status":"processing","failure_reason":null,"price":"2.5000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}}},"sandbox":{"summary":"Clé sandbox : envoi simulé réussi","value":{"data":{"id":50,"campaign_id":null,"sandbox":true,"channel":"sms","phone":"242066000001","text":"Hello","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"sent","failure_reason":null,"price":"0.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":"2026-10-07T09:15:00.000000Z","created_at":"2026-10-07T09:15:00.000000Z"}}},"sandboxEchec":{"summary":"Clé sandbox : numéro d'échec 242000000000","value":{"data":{"id":51,"campaign_id":null,"sandbox":true,"channel":"sms","phone":"242000000000","text":"Hello","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"failed","failure_reason":"Échec simulé (numéro de test sandbox).","price":"0.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/AccountSuspended"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Erreur de validation (`errors` présent) ou erreur métier (`message` seul) : solde insuffisant, Sender ID non configuré, session WhatsApp non connectée.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/Error"}]},"examples":{"validation":{"summary":"Champs manquants","value":{"message":"Le champ numéro de téléphone est obligatoire. (and 1 more error)","errors":{"phone":["Le champ numéro de téléphone est obligatoire."],"text":["Le champ message est obligatoire."]}}},"telephoneInvalide":{"summary":"Numéro invalide","value":{"message":"Le format du champ numéro de téléphone est invalide.","errors":{"phone":["Le format du champ numéro de téléphone est invalide."]}}},"senderIdInconnu":{"summary":"Sender ID inconnu, suspendu ou d'un autre compte","value":{"message":"La valeur de Sender ID est invalide.","errors":{"sender_id":["La valeur de Sender ID est invalide."]}}},"programmationPassee":{"summary":"scheduled_at dans le passé","value":{"message":"Le champ date d'envoi doit être une date postérieure au now.","errors":{"scheduled_at":["Le champ date d'envoi doit être une date postérieure au now."]}}},"mediaSurSms":{"summary":"Média envoyé sur le canal SMS","value":{"message":"Le champ URL du média n'est pas autorisé.","errors":{"media_url":["Le champ URL du média n'est pas autorisé."]}}},"plusieursContenus":{"summary":"Plusieurs contenus spéciaux","value":{"message":"Un seul type de contenu (média, position ou contact) est autorisé par message.","errors":{"channel":["Un seul type de contenu (média, position ou contact) est autorisé par message."]}}},"contactIncomplet":{"summary":"Fiche contact incomplète","value":{"message":"Chaque contact doit fournir soit vcard, soit full_name et phone_number.","errors":{"contacts.0":["Chaque contact doit fournir soit vcard, soit full_name et phone_number."]}}},"soldeInsuffisant":{"summary":"Solde insuffisant","value":{"message":"Solde de crédits insuffisant pour envoyer ce message."}},"senderIdNonConfigure":{"summary":"Aucun Sender ID utilisable","value":{"message":"Configurez le Sender ID de votre compte avant d'envoyer des SMS."},"description":"Aucun `sender_id` fourni et le compte n'a ni Sender ID par défaut ni un unique Sender ID actif."},"whatsappNonConnecte":{"summary":"Session WhatsApp non connectée","value":{"message":"Aucune session WhatsApp connectée. Appairez votre compte WhatsApp avant d'envoyer un message."}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}},"callbacks":{"messageStatus":{"{$request.body#/callback_url}":{"post":{"summary":"Changement de statut du message","description":"Voir le webhook `messageStatus` de la section `webhooks`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageStatusWebhookPayload"}}}},"responses":{"2XX":{"description":"Webhook accepté (toute réponse < 400 est considérée comme un succès)."}}}}}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/messages/bulk":{"post":{"tags":["Messages","Campagnes"],"operationId":"sendBulkMessages","summary":"Envoyer en masse (campagne)","description":"Envoie le même message (personnalisé par destinataire via les variables) à une liste de destinataires issue de `phones`, d'un fichier de contacts (`file`), de groupes de contacts (`group_ids`) et/ou de tags (`tags`). Les numéros sont fusionnés et dédupliqués ; **10 000 destinataires maximum en SMS**, **500 sur WhatsApp**.\n\nLe coût total (prix unitaire × nombre de destinataires) est vérifié et débité **en une seule opération** : en cas de solde insuffisant, aucun message n'est créé. En production, une **campagne** est créée (identifiant dans `meta.campaign_id`, consultable via `GET /campaigns/{campaign}`) et ses messages sont remis en file d'envoi en arrière-plan : leur statut évolue ensuite comme celui de tout message. Sur WhatsApp, chaque message passe par l'anti-blocage, qui espace les envois (champ `deferred_until`). En sandbox, rien n'est débité ni aucune campagne créée.\n\nPour importer un fichier, envoyez la requête en `multipart/form-data`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendBulkMessageRequest"},"examples":{"liste":{"summary":"Liste de numéros","value":{"phones":["242066000001","242066000002"],"text":"Promo : -20 % ce week-end !"}},"campagneComplete":{"summary":"Campagne nommée avec groupes, tags et personnalisation","value":{"campaign_name":"Promo Black Friday","phones":["242066000001","242066000002","242066000003"],"group_ids":[4],"tags":["vip"],"text":"Bonjour {{ prenom|cher client }}, -20 % ce week-end !","sender_id":"MaMarque","callback_url":"https://votre-domaine.com/webhooks/sms-status"}},"programme":{"summary":"Campagne programmée","value":{"campaign_name":"Black Friday J-0","tags":["newsletter"],"text":"C'est parti : -30 % sur tout le site aujourd'hui !","scheduled_at":"2026-11-27T07:00:00Z"}},"whatsapp":{"summary":"Envoi WhatsApp avec média","value":{"channel":"whatsapp","campaign_name":"Catalogue de rentrée","group_ids":[4],"text":"Bonjour {{ prenom|cher client }}, voici notre nouveau catalogue.","media_url":"https://cdn.votre-domaine.com/catalogue.pdf","media_type":"file"}}}},"multipart/form-data":{"schema":{"$ref":"#/components/schemas/SendBulkMessageMultipartRequest"},"encoding":{"file":{"contentType":"text/csv, text/plain, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}},"examples":{"fichier":{"summary":"Fichier de contacts","value":{"file":"(contenu binaire de contacts.xlsx)","text":"Bonjour depuis mon application","sender_id":"MaMarque"}}}}}},"responses":{"201":{"description":"Messages créés (un par destinataire).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkMessageResponse"},"examples":{"production":{"summary":"Envoi en production","value":{"data":[{"id":43,"campaign_id":17,"sandbox":false,"channel":"sms","phone":"242066000001","text":"Promo : -20 % ce week-end !","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"processing","failure_reason":null,"price":"30.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"},{"id":44,"campaign_id":17,"sandbox":false,"channel":"sms","phone":"242066000002","text":"Promo : -20 % ce week-end !","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"processing","failure_reason":null,"price":"30.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}],"meta":{"count":2,"skipped_rows":0,"campaign_id":17}}},"fichier":{"summary":"Import d'un fichier (ligne d'en-tête ignorée)","value":{"data":[{"id":45,"campaign_id":18,"sandbox":false,"channel":"sms","phone":"242066000001","text":"Bonjour depuis mon application","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"processing","failure_reason":null,"price":"30.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}],"meta":{"count":1,"skipped_rows":1,"campaign_id":18}}},"sandbox":{"summary":"Clé sandbox","value":{"data":[{"id":60,"campaign_id":null,"sandbox":true,"channel":"sms","phone":"242066000001","text":"Hello","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"sent","failure_reason":null,"price":"0.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":"2026-10-07T09:15:00.000000Z","created_at":"2026-10-07T09:15:00.000000Z"},{"id":61,"campaign_id":null,"sandbox":true,"channel":"sms","phone":"242000000000","text":"Hello","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"failed","failure_reason":"Échec simulé (numéro de test sandbox).","price":"0.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}],"meta":{"count":2,"skipped_rows":0,"campaign_id":null}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/AccountSuspended"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Erreur de validation ou erreur métier (solde insuffisant pour l'ensemble des destinataires, Sender ID non configuré).","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/Error"}]},"examples":{"sansDestinataire":{"summary":"Aucune source de destinataires","value":{"message":"Fournissez une liste de numéros ou un fichier de contacts. (and 1 more error)","errors":{"phones":["Fournissez une liste de numéros ou un fichier de contacts."],"file":["Fournissez une liste de numéros ou un fichier de contacts."]}}},"numeroInvalide":{"summary":"Numéro invalide dans la liste","value":{"message":"Le numéro 12 est invalide (8 à 15 chiffres attendus).","errors":{"phones.1":["Le numéro 12 est invalide (8 à 15 chiffres attendus)."]}}},"tropDeDestinataires":{"summary":"Plus de 500 destinataires après fusion des sources","value":{"message":"Un envoi en masse est limité à 500 destinataires.","errors":{"phones":["Un envoi en masse est limité à 500 destinataires."]}}},"groupeInconnu":{"summary":"Groupe d'un autre compte ou inexistant","value":{"message":"Ce groupe de contacts est introuvable.","errors":{"group_ids.0":["Ce groupe de contacts est introuvable."]}}},"audienceVide":{"summary":"Groupes/tags sans contact","value":{"message":"Aucun contact ne correspond aux groupes ou tags sélectionnés.","errors":{"group_ids":["Aucun contact ne correspond aux groupes ou tags sélectionnés."]}}},"fichierVide":{"summary":"Fichier sans numéro valide","value":{"message":"Aucun numéro de téléphone valide trouvé dans le fichier.","errors":{"file":["Aucun numéro de téléphone valide trouvé dans le fichier."]}}},"formatFichier":{"summary":"Format de fichier refusé","value":{"message":"Le fichier de contacts doit être au format CSV ou XLSX. (and 1 more error)","errors":{"file":["Le fichier de contacts doit être au format CSV ou XLSX.","Le fichier de contacts doit être au format CSV ou XLSX."]}}},"soldeInsuffisant":{"summary":"Solde insuffisant","value":{"message":"Solde de crédits insuffisant pour envoyer ce message."}},"senderIdNonConfigure":{"summary":"Aucun Sender ID utilisable","value":{"message":"Configurez le Sender ID de votre compte avant d'envoyer des SMS."}},"tropDeNumeros":{"summary":"Tableau `phones` de plus de 500 éléments","value":{"message":"Le champ numéros ne doit pas contenir plus de 500 éléments.","errors":{"phones":["Le champ numéros ne doit pas contenir plus de 500 éléments."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/messages/{message}":{"get":{"tags":["Messages"],"operationId":"getMessage","summary":"Récupérer un message","description":"Renvoie un message du compte, avec son statut à jour. Une clé sandbox ne voit que les messages sandbox, une clé de production que les messages réels.","parameters":[{"name":"message","in":"path","required":true,"description":"Identifiant (`id`) du message.","schema":{"type":"integer"},"example":42}],"responses":{"200":{"description":"Le message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"},"examples":{"envoye":{"summary":"SMS envoyé","value":{"data":{"id":42,"campaign_id":null,"sandbox":false,"channel":"sms","phone":"242066000000","text":"Bonjour depuis mon application","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"sent","failure_reason":null,"price":"30.0000","currency":"XAF","scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":"2026-10-07T09:15:04.000000Z","created_at":"2026-10-07T09:15:00.000000Z"}}},"echoue":{"summary":"SMS en échec","value":{"data":{"id":44,"campaign_id":null,"sandbox":false,"channel":"sms","phone":"242069999999","text":"Bonjour depuis mon application","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"failed","failure_reason":"Wirepick delivery report: INV (Invalid destination address)","price":"30.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}}},"whatsappEtale":{"summary":"WhatsApp étalé par l'anti-blocage","value":{"data":{"id":49,"campaign_id":null,"sandbox":false,"channel":"whatsapp","phone":"242066000000","text":"Bonjour sur WhatsApp","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"WhatsApp","status":"processing","failure_reason":null,"price":"2.5000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":"2026-10-07T09:16:40.000000Z","sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/MessageNotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/campaigns":{"get":{"tags":["Campagnes"],"operationId":"listCampaigns","summary":"Lister les campagnes","description":"Liste les campagnes du compte (créées par `POST /messages/bulk` ou depuis l'application web) avec leurs statistiques, de la plus récente à la plus ancienne, **25 par page**.","parameters":[{"name":"page","in":"query","required":false,"description":"Numéro de page (à partir de 1).","schema":{"type":"integer","minimum":1,"default":1},"example":1}],"responses":{"200":{"description":"Page de campagnes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignCollection"},"examples":{"page":{"summary":"Première page","value":{"data":[{"id":17,"name":"Promo Black Friday","channel":"sms","text":"Bonjour {{ prenom|cher client }}, -20 % ce week-end !","from":"MaMarque","recipients_count":1250,"scheduled_at":"2026-10-06T08:00:00.000000Z","created_at":"2026-10-06T08:00:00.000000Z","stats":{"total":1250,"sent":1198,"failed":32,"pending":20,"scheduled":0,"delivery_rate":95.8,"failure_rate":2.6,"cost":36540}}],"links":{"first":"https://sender.gesflow.biz/api/v1/campaigns?page=1","last":"https://sender.gesflow.biz/api/v1/campaigns?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[{"url":null,"label":"&laquo; Précédent","page":null,"active":false},{"url":"https://sender.gesflow.biz/api/v1/campaigns?page=1","label":"1","page":1,"active":true},{"url":null,"label":"Suivant &raquo;","page":null,"active":false}],"path":"https://sender.gesflow.biz/api/v1/campaigns","per_page":25,"to":1,"total":1}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/campaigns/{campaign}":{"get":{"tags":["Campagnes"],"operationId":"getCampaign","summary":"Récupérer une campagne","description":"Renvoie une campagne du compte et ses statistiques calculées en temps réel. Les messages individuels de la campagne sont visibles dans `GET /messages` (champ `campaign_id`).","parameters":[{"name":"campaign","in":"path","required":true,"description":"Identifiant de la campagne (`meta.campaign_id` de l'envoi en masse).","schema":{"type":"integer"},"example":17}],"responses":{"200":{"description":"La campagne.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignResponse"},"examples":{"campagne":{"summary":"Campagne terminée","value":{"data":{"id":17,"name":"Promo Black Friday","channel":"sms","text":"Bonjour {{ prenom|cher client }}, -20 % ce week-end !","from":"MaMarque","recipients_count":1250,"scheduled_at":"2026-10-06T08:00:00.000000Z","created_at":"2026-10-06T08:00:00.000000Z","stats":{"total":1250,"sent":1198,"failed":32,"pending":20,"scheduled":0,"delivery_rate":95.8,"failure_rate":2.6,"cost":36540}}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/CampaignNotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/inbound-messages":{"get":{"tags":["Messages entrants"],"operationId":"listInboundMessages","summary":"Lister les messages reçus","description":"Liste les messages reçus par le compte (réponses SMS et messages WhatsApp entrants), du plus récent au plus ancien (`received_at`), **25 par page**. Les liens de pagination ne conservent pas les filtres.","parameters":[{"name":"phone","in":"query","required":false,"description":"Filtre exact sur le numéro de l'expéditeur. Doit contenir uniquement 8 à 15 chiffres (non nettoyé : `+242…` est refusé en 422).","schema":{"type":"string","pattern":"^[0-9]{8,15}$"},"example":"242066000000"},{"name":"since","in":"query","required":false,"description":"Ne renvoie que les messages reçus à partir de cette date (incluse, UTC). Toute date reconnue est acceptée ; le format `AAAA-MM-JJ HH:MM:SS` est recommandé (la valeur est comparée telle quelle à la date stockée).","schema":{"type":"string"},"example":"2026-10-01 00:00:00"},{"name":"page","in":"query","required":false,"description":"Numéro de page (à partir de 1).","schema":{"type":"integer","minimum":1,"default":1},"example":1}],"responses":{"200":{"description":"Page de messages reçus.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboundMessageCollection"},"examples":{"page":{"summary":"Messages reçus","value":{"data":[{"id":129,"channel":"whatsapp","phone":"242066000000","to":null,"text":"Voici la photo du colis","media_url":"https://waha.example.com/api/files/default/3EB0C767D26A1D.jpeg","media_mimetype":"image/jpeg","received_at":"2026-10-06T09:20:00.000000Z","read_at":"2026-10-06T09:25:00.000000Z"},{"id":128,"channel":"sms","phone":"242066000000","to":"MaMarque","text":"Oui, je confirme ma réservation","media_url":null,"media_mimetype":null,"received_at":"2026-10-06T09:15:00.000000Z","read_at":null}],"links":{"first":"https://sender.gesflow.biz/api/v1/inbound-messages?page=1","last":"https://sender.gesflow.biz/api/v1/inbound-messages?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[{"url":null,"label":"&laquo; Précédent","page":null,"active":false},{"url":"https://sender.gesflow.biz/api/v1/inbound-messages?page=1","label":"1","page":1,"active":true},{"url":null,"label":"Suivant &raquo;","page":null,"active":false}],"path":"https://sender.gesflow.biz/api/v1/inbound-messages","per_page":25,"to":2,"total":2}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"422":{"description":"Filtre invalide.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"filtres":{"summary":"Filtres invalides","value":{"message":"Le format du champ numéro de téléphone est invalide. (and 1 more error)","errors":{"phone":["Le format du champ numéro de téléphone est invalide."],"since":["Le champ since n'est pas une date valide."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/verify/send":{"post":{"tags":["OTP (Vérification)"],"operationId":"sendVerificationCode","summary":"Envoyer un code OTP","description":"Génère un code numérique aléatoire, l'envoie par SMS ou WhatsApp et n'en conserve qu'une empreinte. Le message porteur du code est un message ordinaire : il est **facturé** au tarif du canal (sauf sandbox) et soumis aux mêmes erreurs métier (solde, Sender ID, session WhatsApp).\n\nUn nouveau code **invalide** le code précédent encore en attente pour ce numéro. 5 tentatives de vérification maximum par code.\n\n**Limites** (par compte et par numéro) : 30 secondes minimum entre deux codes, 5 codes maximum par fenêtre de 10 minutes → `429`. La limite est contrôlée et comptée **avant** l'envoi : une requête qui échoue ensuite en `422` métier (solde, Sender ID, session WhatsApp) compte tout de même dans les 5 codes de la fenêtre.\n\n**Sandbox** : rien n'est envoyé ni débité et le code est renvoyé dans `data.code`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendOtpRequest"},"examples":{"simple":{"summary":"Code SMS par défaut (6 chiffres, 5 minutes)","value":{"phone":"242066000000"}},"personnalise":{"summary":"Code personnalisé","value":{"phone":"242066000000","channel":"sms","code_length":4,"expires_in":600,"template":"MyApp : votre code est {{ code }} ({{ minutes }} min).","sender_id":"MaMarque"}},"whatsapp":{"summary":"Code par WhatsApp","value":{"phone":"242066000000","channel":"whatsapp"}}}}}},"responses":{"201":{"description":"Code envoyé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpVerificationResponse"},"examples":{"production":{"summary":"Clé de production","value":{"data":{"id":"01K6Z3X8T9Q4M2N7B5V1C0D8EF","phone":"242066000000","channel":"sms","status":"pending","message_id":512,"expires_at":"2026-10-07T09:20:00.000000Z","sandbox":false}}},"sandbox":{"summary":"Clé sandbox (code renvoyé)","value":{"data":{"id":"01K6Z3X8T9Q4M2N7B5V1C0D8EF","phone":"242066000000","channel":"sms","status":"pending","message_id":513,"expires_at":"2026-10-07T09:20:00.000000Z","sandbox":true,"code":"482913"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"403":{"$ref":"#/components/responses/AccountSuspended"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Erreur de validation ou erreur métier.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/Error"}]},"examples":{"telephone":{"summary":"Numéro invalide","value":{"message":"Le numéro de téléphone est invalide (8 à 15 chiffres attendus).","errors":{"phone":["Le numéro de téléphone est invalide (8 à 15 chiffres attendus)."]}}},"modele":{"summary":"Modèle sans {{ code }}","value":{"message":"Le modèle doit contenir la variable {{ code }}.","errors":{"template":["Le modèle doit contenir la variable {{ code }}."]}}},"longueur":{"summary":"Longueur hors limites","value":{"message":"La valeur de code length doit être comprise entre 4 et 8.","errors":{"code_length":["La valeur de code length doit être comprise entre 4 et 8."]}}},"soldeInsuffisant":{"summary":"Solde insuffisant","value":{"message":"Solde de crédits insuffisant pour envoyer ce message."}},"senderIdNonConfigure":{"summary":"Aucun Sender ID utilisable","value":{"message":"Configurez le Sender ID de votre compte avant d'envoyer des SMS."}},"whatsappNonConnecte":{"summary":"Session WhatsApp non connectée","value":{"message":"Aucune session WhatsApp connectée. Appairez votre compte WhatsApp avant d'envoyer un message."}}}}}},"429":{"description":"Trop de codes envoyés à ce numéro. Également renvoyé quand le compte dépasse la limite de débit globale de l’API (600 requêtes par minute).","headers":{"Retry-After":{"description":"Délai d'attente en secondes.","schema":{"type":"integer"},"example":24}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpThrottledError"},"examples":{"limite":{"summary":"Limite atteinte","value":{"message":"Trop de codes envoyés à ce numéro. Réessayez dans 24 secondes.","retry_after":24}}}}}}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/verify/check":{"post":{"tags":["OTP (Vérification)"],"operationId":"checkVerificationCode","summary":"Vérifier un code OTP","description":"Vérifie le code saisi par l'utilisateur contre le **dernier** code non encore approuvé envoyé à ce numéro (avec le même type de clé, sandbox ou production). Répond **toujours 200** quand la requête est valide : lisez `valid` et `status`. Chaque code faux consomme une tentative ; un code approuvé ne peut plus être réutilisé.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckOtpRequest"},"examples":{"verification":{"summary":"Vérification","value":{"phone":"242066000000","code":"482913"}}}}}},"responses":{"200":{"description":"Résultat de la vérification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpCheckResponse"},"examples":{"approuve":{"summary":"Code correct","value":{"data":{"id":"01K6Z3X8T9Q4M2N7B5V1C0D8EF","phone":"242066000000","valid":true,"status":"approved","attempts_remaining":4}}},"invalide":{"summary":"Code incorrect","value":{"data":{"id":"01K6Z3X8T9Q4M2N7B5V1C0D8EF","phone":"242066000000","valid":false,"status":"invalid","attempts_remaining":3}}},"expire":{"summary":"Code expiré","value":{"data":{"id":"01K6Z3X8T9Q4M2N7B5V1C0D8EF","phone":"242066000000","valid":false,"status":"expired","attempts_remaining":5}}},"tentativesEpuisees":{"summary":"Tentatives épuisées","value":{"data":{"id":"01K6Z3X8T9Q4M2N7B5V1C0D8EF","phone":"242066000000","valid":false,"status":"max_attempts_reached","attempts_remaining":0}}},"introuvable":{"summary":"Aucun code en attente","value":{"data":{"id":null,"phone":"242066000000","valid":false,"status":"not_found","attempts_remaining":0}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Erreur de validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"code":{"summary":"Code mal formé","value":{"message":"Le code doit contenir entre 4 et 8 chiffres.","errors":{"code":["Le code doit contenir entre 4 et 8 chiffres."]}}},"manquants":{"summary":"Champs manquants","value":{"message":"Le champ numéro de téléphone est obligatoire. (and 1 more error)","errors":{"phone":["Le champ numéro de téléphone est obligatoire."],"code":["Le champ code est obligatoire."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/whatsapp/check-number":{"get":{"tags":["WhatsApp"],"operationId":"checkWhatsappNumber","summary":"Vérifier qu'un numéro a WhatsApp","description":"Interroge WhatsApp, via la session connectée du compte, pour savoir si le numéro possède un compte WhatsApp. Aucun message n'est envoyé et rien n'est débité. Requiert une session WhatsApp connectée, **y compris avec une clé sandbox**.","parameters":[{"name":"phone","in":"query","required":true,"description":"Numéro à vérifier ; les caractères non numériques sont supprimés, 8 à 15 chiffres.","schema":{"type":"string"},"example":"242066000000"}],"responses":{"200":{"description":"Résultat.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsappNumberCheckResponse"},"examples":{"oui":{"summary":"Numéro WhatsApp","value":{"data":{"phone":"242066000000","has_whatsapp":true}}},"non":{"summary":"Numéro sans WhatsApp","value":{"data":{"phone":"242066000000","has_whatsapp":false}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"422":{"description":"Numéro invalide ou session WhatsApp non connectée.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ValidationError"},{"$ref":"#/components/schemas/Error"}]},"examples":{"telephone":{"summary":"Numéro invalide","value":{"message":"Le format du champ numéro de téléphone est invalide.","errors":{"phone":["Le format du champ numéro de téléphone est invalide."]}}},"whatsappNonConnecte":{"summary":"Session non connectée","value":{"message":"Aucune session WhatsApp connectée. Appairez votre compte WhatsApp avant d'envoyer un message."}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"},"503":{"description":"WhatsApp injoignable ou erreur lors de la vérification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"indisponible":{"summary":"Service indisponible","value":{"message":"Impossible de vérifier ce numéro pour le moment. Réessayez dans quelques instants."}}}}}}}}},"/api/v1/payments/fees-config":{"get":{"tags":["Paiements"],"operationId":"getPaymentFeesConfig","summary":"Configuration des frais de recharge","description":"Route **publique** (sans authentification). Renvoie les paramètres permettant de calculer le montant total d'une recharge :\n\n- `taxes = montant × tax_vat_percentage × (1 + tax_ca_percentage)`\n- `frais = (montant + taxes) × provider_fee_percentage + provider_fee_fixed`\n- `total = montant + taxes + frais`\n\nExemple pour 5000 : taxes 945, frais 233,075, total 6178,075 (6178 demandés au payeur).","security":[],"responses":{"200":{"description":"Configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentFeesConfig"},"examples":{"defaut":{"summary":"Configuration","value":{"min_amount":500,"provider_fee_percentage":0.035,"provider_fee_fixed":25,"tax_vat_percentage":0.18,"tax_ca_percentage":0.05,"country":"cg","currency":"XAF","phone_country_code":"+242"}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/payments/initiate":{"post":{"tags":["Paiements"],"operationId":"initiatePayment","summary":"Initier une recharge Mobile Money","description":"Crée une recharge Mobile Money (MTN ou Airtel) et envoie la demande de paiement au prestataire. Le payeur valide ensuite le paiement sur son téléphone (USSD). La réponse `201` renvoie le paiement au statut `processing` (demande transmise) ou directement `failed` (prestataire injoignable ou réponse invalide, raison dans `failure_message`) : un échec côté prestataire ne produit **pas** d'erreur HTTP.\n\nSuivez ensuite le statut avec `GET /payments/{payment}`. En cas de succès, `base_amount` est crédité sur le solde.\n\n⚠️ Aucun mode sandbox : même avec une clé de test, un vrai paiement est déclenché.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitiatePaymentRequest"},"examples":{"mtn":{"summary":"Recharge MTN","value":{"amount":5000,"phone_number":"066000000","operator":"mtn"}},"airtel":{"summary":"Recharge Airtel, numéro international","value":{"amount":10000,"phone_number":"+242 05 500 00 00","operator":"airtel"}}}}}},"responses":{"201":{"description":"Paiement créé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentResponse"},"examples":{"enCours":{"summary":"Demande transmise","value":{"data":{"id":31,"reference":"PAY-7QK2M9XA","status":"processing","status_label":"En cours","base_amount":"5000.0000","service_fee":"233.0750","transaction_fee":"945.0000","amount":"6178.0750","currency":"XAF","phone_number":"+242066000000","operator":"mtn","country":"cg","failure_message":null,"created_at":"2026-10-07T09:15:00+00:00","updated_at":"2026-10-07T09:15:01+00:00"}}},"echec":{"summary":"Prestataire injoignable","value":{"data":{"id":32,"reference":"PAY-Z81LQ0WE","status":"failed","status_label":"Échoué","base_amount":"5000.0000","service_fee":"233.0750","transaction_fee":"945.0000","amount":"6178.0750","currency":"XAF","phone_number":"+242066000000","operator":"mtn","country":"cg","failure_message":"Le service de paiement est temporairement injoignable. Veuillez réessayer.","created_at":"2026-10-07T09:15:00+00:00","updated_at":"2026-10-07T09:15:01+00:00"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Erreur de validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"invalide":{"summary":"Données invalides","value":{"message":"La valeur de montant doit être au moins 500. (and 2 more errors)","errors":{"amount":["La valeur de montant doit être au moins 500."],"phone_number":["Le format du champ numéro de téléphone est invalide."],"operator":["La valeur de opérateur est invalide."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/payments/me":{"get":{"tags":["Paiements"],"operationId":"listPayments","summary":"Historique des recharges","description":"Liste les recharges du compte (tous utilisateurs), de la plus récente à la plus ancienne. Le statut n'est **pas** rafraîchi auprès du prestataire dans cette liste (utilisez `GET /payments/{payment}`).","parameters":[{"name":"page","in":"query","required":false,"description":"Numéro de page (à partir de 1).","schema":{"type":"integer","minimum":1,"default":1},"example":1},{"name":"per_page","in":"query","required":false,"description":"Nombre d'éléments par page (15 par défaut ; 0 ou valeur non numérique → 15). Aucune limite haute n'est appliquée.","schema":{"type":"integer","minimum":1,"default":15},"example":15}],"responses":{"200":{"description":"Page de paiements.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentCollection"},"examples":{"page":{"summary":"Historique","value":{"data":[{"id":31,"reference":"PAY-7QK2M9XA","status":"success","status_label":"Réussi","base_amount":"5000.0000","service_fee":"233.0750","transaction_fee":"945.0000","amount":"6178.0750","currency":"XAF","phone_number":"+242066000000","operator":"mtn","country":"cg","failure_message":null,"created_at":"2026-10-07T09:15:00+00:00","updated_at":"2026-10-07T09:16:12+00:00"},{"id":32,"reference":"PAY-Z81LQ0WE","status":"failed","status_label":"Échoué","base_amount":"5000.0000","service_fee":"233.0750","transaction_fee":"945.0000","amount":"6178.0750","currency":"XAF","phone_number":"+242066000000","operator":"mtn","country":"cg","failure_message":"Le service de paiement est temporairement injoignable. Veuillez réessayer.","created_at":"2026-10-07T09:15:00+00:00","updated_at":"2026-10-07T09:15:01+00:00"}],"links":{"first":"https://sender.gesflow.biz/api/v1/payments/me?page=1","last":"https://sender.gesflow.biz/api/v1/payments/me?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[{"url":null,"label":"&laquo; Précédent","page":null,"active":false},{"url":"https://sender.gesflow.biz/api/v1/payments/me?page=1","label":"1","page":1,"active":true},{"url":null,"label":"Suivant &raquo;","page":null,"active":false}],"path":"https://sender.gesflow.biz/api/v1/payments/me","per_page":15,"to":2,"total":2}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/payments/{payment}":{"get":{"tags":["Paiements"],"operationId":"getPayment","summary":"Statut d'une recharge","description":"Renvoie une recharge du compte. Si elle est encore `pending` ou `processing`, son statut est d'abord **rafraîchi auprès du prestataire** : l'appel peut donc la faire passer à `success` (et créditer le solde) ou `failed`. Si le prestataire est injoignable, le statut actuel est renvoyé sans erreur.","parameters":[{"name":"payment","in":"path","required":true,"description":"Identifiant (`id`) du paiement.","schema":{"type":"integer"},"example":31}],"responses":{"200":{"description":"Le paiement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentResponse"},"examples":{"succes":{"summary":"Paiement réussi","value":{"data":{"id":31,"reference":"PAY-7QK2M9XA","status":"success","status_label":"Réussi","base_amount":"5000.0000","service_fee":"233.0750","transaction_fee":"945.0000","amount":"6178.0750","currency":"XAF","phone_number":"+242066000000","operator":"mtn","country":"cg","failure_message":null,"created_at":"2026-10-07T09:15:00+00:00","updated_at":"2026-10-07T09:16:12+00:00"}}},"enCours":{"summary":"En attente de validation","value":{"data":{"id":31,"reference":"PAY-7QK2M9XA","status":"processing","status_label":"En cours","base_amount":"5000.0000","service_fee":"233.0750","transaction_fee":"945.0000","amount":"6178.0750","currency":"XAF","phone_number":"+242066000000","operator":"mtn","country":"cg","failure_message":null,"created_at":"2026-10-07T09:15:00+00:00","updated_at":"2026-10-07T09:15:01+00:00"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/PaymentNotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/account":{"get":{"tags":["Compte"],"operationId":"getAccount","summary":"Consulter le solde et les tarifs","description":"Renvoie le solde de crédits du compte, les prix unitaires qui lui sont appliqués (SMS : prix spécifique du compte ou tarif global ; WhatsApp : tarif global) et le nombre de messages que le solde couvre encore.","responses":{"200":{"description":"Le compte.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResponse"},"examples":{"exemple":{"summary":"Le compte.","value":{"data":{"id":12,"name":"Boutique Mbote","credit_balance":"12500.0000","currency":"XAF","prices":{"sms":"20.0000","whatsapp":"2.5000"},"remaining":{"sms":625,"whatsapp":5000}}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/account/transactions":{"get":{"tags":["Compte"],"operationId":"listCreditTransactions","summary":"Historique des mouvements du solde","description":"Liste les mouvements du solde de crédits, du plus récent au plus ancien : chaque débit, ajustement, remboursement ou recharge, avec le solde qu’il a laissé (`balance_after`). La somme des montants est égale au solde du compte.","parameters":[{"name":"type","in":"query","required":false,"description":"Ne garder qu’un type de mouvement.","schema":{"$ref":"#/components/schemas/CreditTransactionType"},"example":"message_refund"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"example":1}],"responses":{"200":{"description":"Les mouvements.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditTransactionCollection"},"examples":{"exemple":{"summary":"Les mouvements.","value":{"data":[{"id":982,"type":"message_charge","label":"Envoi de message","amount":"-20.0000","balance_after":"12480.0000","description":"SMS vers 242066000001","reference":{"type":"message","id":42},"created_at":"2026-10-07T09:15:00.000000Z"},{"id":981,"type":"payment","label":"Recharge Mobile Money","amount":"10000.0000","balance_after":"12500.0000","description":"Recharge Mobile Money (mtn) — PAY-20261007-AB12","reference":{"type":"payment","id":77},"created_at":"2026-10-07T08:02:11.000000Z"}],"links":{"first":"https://sender.gesflow.biz/api/v1/account/transactions?page=1","last":"https://sender.gesflow.biz/api/v1/account/transactions?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[],"path":"https://sender.gesflow.biz/api/v1/account/transactions","per_page":25,"to":2,"total":2}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"422":{"description":"Type inconnu.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"validation":{"summary":"Erreur de validation","value":{"message":"Le champ type sélectionné est invalide.","errors":{"type":["Le champ type sélectionné est invalide."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/messages/{message}/cancel":{"post":{"tags":["Messages"],"operationId":"cancelMessage","summary":"Annuler un message programmé","description":"Annule un message encore programmé (`status` = `scheduled`, `scheduled_at` dans le futur) et rembourse son prix. Le message passe en `failed` avec `failure_reason` = « Envoi programmé annulé par l'utilisateur. » (le webhook de statut est envoyé). Un message déjà en cours d'envoi ou envoyé ne peut plus être annulé.","parameters":[{"name":"message","in":"path","required":true,"description":"Identifiant du message.","schema":{"type":"integer"},"example":42},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Le message annulé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageResponse"},"examples":{"exemple":{"summary":"Le message annulé.","value":{"data":{"id":42,"campaign_id":null,"sandbox":false,"channel":"sms","phone":"242066000001","text":"Rappel : votre rendez-vous est demain à 10 h.","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"failed","failure_reason":"Envoi programmé annulé par l'utilisateur.","price":"20.0000","currency":null,"scheduled_at":"2026-10-08T08:00:00.000000Z","deferred_until":null,"sent_at":null,"replayed_at":null,"replayable":true,"created_at":"2026-10-07T09:15:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/MessageNotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Le message n’est plus programmé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"refused":{"summary":"Refusé","value":{"message":"Ce message n'est plus programmé : il ne peut plus être annulé."}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/campaigns/{campaign}/cancel":{"post":{"tags":["Campagnes"],"operationId":"cancelCampaign","summary":"Annuler les envois programmés d’une campagne","description":"Annule tous les messages encore programmés de la campagne et rembourse leur prix en un seul mouvement de solde. Les messages déjà envoyés ou en cours d’envoi ne sont pas concernés. `meta.cancelled` indique le nombre de messages annulés. Une clé sandbox ne peut pas annuler une campagne (les campagnes sont toujours réelles).","parameters":[{"name":"campaign","in":"path","required":true,"description":"Identifiant de la campagne.","schema":{"type":"integer"},"example":17},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"La campagne après annulation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignCancelResponse"},"examples":{"exemple":{"summary":"La campagne après annulation.","value":{"data":{"id":17,"name":"Black Friday J-0","channel":"sms","text":"C'est parti : -30 % sur tout le site aujourd'hui !","from":"MaMarque","recipients_count":1250,"scheduled_at":"2026-11-27T07:00:00.000000Z","created_at":"2026-11-20T10:00:00.000000Z","stats":{"total":1250,"sent":0,"failed":1250,"pending":0,"scheduled":0,"delivery_rate":0,"failure_rate":100,"cost":0}},"meta":{"cancelled":1250}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/CampaignNotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Plus rien à annuler, ou clé sandbox.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"refused":{"summary":"Refusé","value":{"message":"Cette campagne n'a plus de message programmé à annuler."}},"sandbox":{"summary":"Clé sandbox","value":{"message":"Une clé sandbox ne peut pas annuler une campagne réelle : utilisez une clé de production."}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/contacts":{"get":{"tags":["Contacts"],"operationId":"listContacts","summary":"Lister les contacts","description":"Liste les contacts du compte, du plus récent au plus ancien, avec leurs groupes.","parameters":[{"name":"search","in":"query","required":false,"description":"Recherche dans le nom ou le numéro.","schema":{"type":"string","maxLength":100},"example":"Dupont"},{"name":"group_id","in":"query","required":false,"description":"Contacts d’un groupe.","schema":{"type":"integer"},"example":4},{"name":"tag","in":"query","required":false,"description":"Contacts portant ce tag (insensible à la casse).","schema":{"type":"string","maxLength":50},"example":"vip"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"example":1}],"responses":{"200":{"description":"Les contacts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactCollection"},"examples":{"exemple":{"summary":"Les contacts.","value":{"data":[{"id":311,"name":"Jean Dupont","phone":"242066000001","tags":["client","vip"],"custom_fields":{"ville":"Brazzaville"},"groups":[{"id":4,"name":"Clients VIP","description":null,"created_at":"2026-09-01T10:00:00.000000Z"}],"created_at":"2026-09-01T10:00:00.000000Z","updated_at":"2026-09-15T08:30:00.000000Z"}],"links":{"first":"https://sender.gesflow.biz/api/v1/contacts?page=1","last":"https://sender.gesflow.biz/api/v1/contacts?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[],"path":"https://sender.gesflow.biz/api/v1/contacts","per_page":25,"to":1,"total":1}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"tags":["Contacts"],"operationId":"createContact","summary":"Créer un contact","description":"Ajoute un contact au carnet du compte. Son nom et ses champs personnalisés alimentent les variables de personnalisation des messages qui lui sont envoyés.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactRequest"},"examples":{"contact":{"summary":"Contact complet","value":{"name":"Jean Dupont","phone":"242066000001","tags":["client","vip"],"custom_fields":{"ville":"Brazzaville"},"group_ids":[4]}}}}}},"responses":{"201":{"description":"Le contact créé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"},"examples":{"exemple":{"summary":"Le contact créé.","value":{"data":{"id":311,"name":"Jean Dupont","phone":"242066000001","tags":["client","vip"],"custom_fields":{"ville":"Brazzaville"},"groups":[{"id":4,"name":"Clients VIP","description":null,"created_at":"2026-09-01T10:00:00.000000Z"}],"created_at":"2026-09-01T10:00:00.000000Z","updated_at":"2026-09-15T08:30:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Données invalides ou numéro déjà présent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"validation":{"summary":"Erreur de validation","value":{"message":"Ce numéro de téléphone existe déjà dans vos contacts.","errors":{"phone":["Ce numéro de téléphone existe déjà dans vos contacts."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/contacts/{contact}":{"get":{"tags":["Contacts"],"operationId":"getContact","summary":"Récupérer un contact","parameters":[{"name":"contact","in":"path","required":true,"description":"Identifiant du contact.","schema":{"type":"integer"},"example":311}],"responses":{"200":{"description":"Le contact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"},"examples":{"exemple":{"summary":"Le contact.","value":{"data":{"id":311,"name":"Jean Dupont","phone":"242066000001","tags":["client","vip"],"custom_fields":{"ville":"Brazzaville"},"groups":[{"id":4,"name":"Clients VIP","description":null,"created_at":"2026-09-01T10:00:00.000000Z"}],"created_at":"2026-09-01T10:00:00.000000Z","updated_at":"2026-09-15T08:30:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/ContactNotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"put":{"tags":["Contacts"],"operationId":"updateContact","summary":"Modifier un contact","description":"Remplace le nom, le numéro, les tags et les champs personnalisés du contact. Ses groupes ne changent que si `group_ids` est envoyé.","parameters":[{"name":"contact","in":"path","required":true,"description":"Identifiant du contact.","schema":{"type":"integer"},"example":311}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactRequest"},"examples":{"contact":{"summary":"Nouveau nom et numéro","value":{"name":"Jean-Paul Dupont","phone":"242066000009","tags":["client"]}}}}}},"responses":{"200":{"description":"Le contact modifié.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"},"examples":{"exemple":{"summary":"Le contact modifié.","value":{"data":{"id":311,"name":"Jean-Paul Dupont","phone":"242066000009","tags":["client"],"custom_fields":{"ville":"Brazzaville"},"groups":[{"id":4,"name":"Clients VIP","description":null,"created_at":"2026-09-01T10:00:00.000000Z"}],"created_at":"2026-09-01T10:00:00.000000Z","updated_at":"2026-09-15T08:30:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/ContactNotFound"},"422":{"description":"Données invalides.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"validation":{"summary":"Erreur de validation","value":{"message":"Le numéro de téléphone est invalide (8 à 15 chiffres attendus).","errors":{"phone":["Le numéro de téléphone est invalide (8 à 15 chiffres attendus)."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"tags":["Contacts"],"operationId":"deleteContact","summary":"Supprimer un contact","description":"Supprime le contact (et le retire de ses groupes). Les messages déjà envoyés à ce numéro sont conservés.","parameters":[{"name":"contact","in":"path","required":true,"description":"Identifiant du contact.","schema":{"type":"integer"},"example":311}],"responses":{"204":{"description":"Contact supprimé."},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/ContactNotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/contact-groups":{"get":{"tags":["Groupes de contacts"],"operationId":"listContactGroups","summary":"Lister les groupes","description":"Liste les groupes de contacts du compte, par nom, avec leur nombre de contacts.","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"example":1}],"responses":{"200":{"description":"Les groupes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactGroupCollection"},"examples":{"exemple":{"summary":"Les groupes.","value":{"data":[{"id":4,"name":"Clients VIP","description":"Meilleurs clients de l’année","contacts_count":128,"created_at":"2026-09-01T10:00:00.000000Z"}],"links":{"first":"https://sender.gesflow.biz/api/v1/contact-groups?page=1","last":"https://sender.gesflow.biz/api/v1/contact-groups?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[],"path":"https://sender.gesflow.biz/api/v1/contact-groups","per_page":25,"to":1,"total":1}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"tags":["Groupes de contacts"],"operationId":"createContactGroup","summary":"Créer un groupe","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactGroupRequest"},"examples":{"groupe":{"summary":"Nouveau groupe","value":{"name":"Clients VIP","description":"Meilleurs clients de l’année"}}}}}},"responses":{"201":{"description":"Le groupe créé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactGroupResponse"},"examples":{"exemple":{"summary":"Le groupe créé.","value":{"data":{"id":4,"name":"Clients VIP","description":"Meilleurs clients de l’année","contacts_count":0,"created_at":"2026-09-01T10:00:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Nom manquant ou déjà utilisé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"validation":{"summary":"Erreur de validation","value":{"message":"La valeur du champ nom est déjà utilisée.","errors":{"name":["La valeur du champ nom est déjà utilisée."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/contact-groups/{contact_group}":{"put":{"tags":["Groupes de contacts"],"operationId":"updateContactGroup","summary":"Renommer un groupe","parameters":[{"name":"contact_group","in":"path","required":true,"description":"Identifiant du groupe.","schema":{"type":"integer"},"example":4}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactGroupRequest"},"examples":{"groupe":{"summary":"Nouveau nom","value":{"name":"Clients fidèles","description":null}}}}}},"responses":{"200":{"description":"Le groupe modifié.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactGroupResponse"},"examples":{"exemple":{"summary":"Le groupe modifié.","value":{"data":{"id":4,"name":"Clients fidèles","description":null,"contacts_count":128,"created_at":"2026-09-01T10:00:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/ContactGroupNotFound"},"422":{"description":"Nom manquant ou déjà utilisé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"validation":{"summary":"Erreur de validation","value":{"message":"La valeur du champ nom est déjà utilisée.","errors":{"name":["La valeur du champ nom est déjà utilisée."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"tags":["Groupes de contacts"],"operationId":"deleteContactGroup","summary":"Supprimer un groupe","description":"Supprime le groupe. Ses contacts sont conservés.","parameters":[{"name":"contact_group","in":"path","required":true,"description":"Identifiant du groupe.","schema":{"type":"integer"},"example":4}],"responses":{"204":{"description":"Groupe supprimé."},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/ContactGroupNotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/contact-groups/{contact_group}/contacts":{"post":{"tags":["Groupes de contacts"],"operationId":"addContactsToGroup","summary":"Ajouter des contacts à un groupe","description":"Ajoute des contacts du compte au groupe (ceux qui y sont déjà sont ignorés).","parameters":[{"name":"contact_group","in":"path","required":true,"description":"Identifiant du groupe.","schema":{"type":"integer"},"example":4},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactIdsRequest"},"examples":{"contacts":{"summary":"Deux contacts","value":{"contact_ids":[311,312]}}}}}},"responses":{"200":{"description":"Le groupe et son nouveau nombre de contacts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactGroupResponse"},"examples":{"exemple":{"summary":"Le groupe et son nouveau nombre de contacts.","value":{"data":{"id":4,"name":"Clients VIP","description":"Meilleurs clients de l’année","contacts_count":130,"created_at":"2026-09-01T10:00:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/ContactGroupNotFound"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Contact inconnu.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"validation":{"summary":"Erreur de validation","value":{"message":"Ce contact est introuvable.","errors":{"contact_ids.0":["Ce contact est introuvable."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"tags":["Groupes de contacts"],"operationId":"removeContactsFromGroup","summary":"Retirer des contacts d’un groupe","description":"Retire des contacts du groupe. Les contacts eux-mêmes sont conservés.","parameters":[{"name":"contact_group","in":"path","required":true,"description":"Identifiant du groupe.","schema":{"type":"integer"},"example":4}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactIdsRequest"},"examples":{"contacts":{"summary":"Un contact","value":{"contact_ids":[311]}}}}}},"responses":{"200":{"description":"Le groupe et son nouveau nombre de contacts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactGroupResponse"},"examples":{"exemple":{"summary":"Le groupe et son nouveau nombre de contacts.","value":{"data":{"id":4,"name":"Clients VIP","description":"Meilleurs clients de l’année","contacts_count":127,"created_at":"2026-09-01T10:00:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/ContactGroupNotFound"},"422":{"description":"Contact inconnu.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"validation":{"summary":"Erreur de validation","value":{"message":"Ce contact est introuvable.","errors":{"contact_ids.0":["Ce contact est introuvable."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/api/v1/templates":{"get":{"tags":["Modèles"],"operationId":"listTemplates","summary":"Lister les modèles","parameters":[{"name":"search","in":"query","required":false,"description":"Recherche dans le nom ou le texte.","schema":{"type":"string","maxLength":100},"example":"bienvenue"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1},"example":1}],"responses":{"200":{"description":"Les modèles.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateCollection"},"examples":{"exemple":{"summary":"Les modèles.","value":{"data":[{"id":7,"name":"Bienvenue","content":"Bonjour {{ prenom|cher client }}, bienvenue chez nous !","created_at":"2026-09-01T10:00:00.000000Z","updated_at":"2026-09-01T10:00:00.000000Z"}],"links":{"first":"https://sender.gesflow.biz/api/v1/templates?page=1","last":"https://sender.gesflow.biz/api/v1/templates?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[],"path":"https://sender.gesflow.biz/api/v1/templates","per_page":25,"to":1,"total":1}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"post":{"tags":["Modèles"],"operationId":"createTemplate","summary":"Créer un modèle","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateRequest"},"examples":{"modele":{"summary":"Message de bienvenue","value":{"name":"Bienvenue","content":"Bonjour {{ prenom|cher client }}, bienvenue chez nous !"}}}}}},"responses":{"201":{"description":"Le modèle créé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateResponse"},"examples":{"exemple":{"summary":"Le modèle créé.","value":{"data":{"id":7,"name":"Bienvenue","content":"Bonjour {{ prenom|cher client }}, bienvenue chez nous !","created_at":"2026-09-01T10:00:00.000000Z","updated_at":"2026-09-01T10:00:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"409":{"$ref":"#/components/responses/IdempotencyConflict"},"422":{"description":"Nom manquant ou déjà utilisé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"validation":{"summary":"Erreur de validation","value":{"message":"Un modèle avec ce nom existe déjà.","errors":{"name":["Un modèle avec ce nom existe déjà."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/templates/{template}":{"get":{"tags":["Modèles"],"operationId":"getTemplate","summary":"Récupérer un modèle","parameters":[{"name":"template","in":"path","required":true,"description":"Identifiant du modèle.","schema":{"type":"integer"},"example":7}],"responses":{"200":{"description":"Le modèle.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateResponse"},"examples":{"exemple":{"summary":"Le modèle.","value":{"data":{"id":7,"name":"Bienvenue","content":"Bonjour {{ prenom|cher client }}, bienvenue chez nous !","created_at":"2026-09-01T10:00:00.000000Z","updated_at":"2026-09-01T10:00:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/TemplateNotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"put":{"tags":["Modèles"],"operationId":"updateTemplate","summary":"Modifier un modèle","parameters":[{"name":"template","in":"path","required":true,"description":"Identifiant du modèle.","schema":{"type":"integer"},"example":7}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateRequest"},"examples":{"modele":{"summary":"Nouveau texte","value":{"name":"Bienvenue","content":"Bienvenue {{ prenom|cher client }} ! -10 % sur votre première commande."}}}}}},"responses":{"200":{"description":"Le modèle modifié.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateResponse"},"examples":{"exemple":{"summary":"Le modèle modifié.","value":{"data":{"id":7,"name":"Bienvenue","content":"Bienvenue {{ prenom|cher client }} ! -10 % sur votre première commande.","created_at":"2026-09-01T10:00:00.000000Z","updated_at":"2026-09-01T10:00:00.000000Z"}}}}}}},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/TemplateNotFound"},"422":{"description":"Données invalides.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"validation":{"summary":"Erreur de validation","value":{"message":"Le champ content est obligatoire.","errors":{"content":["Le champ content est obligatoire."]}}}}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"tags":["Modèles"],"operationId":"deleteTemplate","summary":"Supprimer un modèle","parameters":[{"name":"template","in":"path","required":true,"description":"Identifiant du modèle.","schema":{"type":"integer"},"example":7}],"responses":{"204":{"description":"Modèle supprimé."},"401":{"$ref":"#/components/responses/Unauthenticated"},"404":{"$ref":"#/components/responses/TemplateNotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}}},"webhooks":{"messageStatus":{"post":{"tags":["Messages"],"operationId":"messageStatusWebhook","summary":"Changement de statut d'un message","description":"Envoyé à l'URL `callback_url` fournie lors de l'envoi (`POST /messages` ou `POST /messages/bulk`), à **chaque changement de statut** du message, par exemple `scheduled → processing`, `processing → sent`, `processing → failed`, ou `sent → failed` si l'opérateur signale un échec après l'envoi. La création du message ne déclenche pas de webhook.\n\nLe corps est l'objet message complet (mêmes champs que `GET /messages/{message}`, sans enveloppe `data`), lu **au moment de l'envoi du webhook** : si plusieurs changements sont rapprochés, plusieurs webhooks peuvent porter le même statut final. Utilisez `id` pour rapprocher le message, et traitez les webhooks de façon idempotente.\n\nLes messages sandbox déclenchent aussi ce webhook (champ `sandbox: true`).\n\n**Livraison** : requête `POST` JSON, en-têtes `Content-Type: application/json` et `Accept: application/json`. Chaque requête est **signée** (en-têtes `X-Gesflow-*`, voir « Webhooks » dans l’introduction) : vérifiez la signature avant de traiter le corps. Délai maximal de réponse : **10 secondes**. Seule une réponse **2xx** est un succès (les redirections ne sont pas suivies) (le corps de la réponse est ignoré). Une réponse 4xx/5xx, un timeout ou une erreur réseau déclenche une nouvelle tentative : **3 tentatives au total**, espacées de **60 secondes**. Si toutes échouent, les propriétaires du compte reçoivent un e-mail d'alerte (limité par compte et par URL).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageStatusWebhookPayload"},"examples":{"envoye":{"summary":"SMS envoyé","value":{"id":42,"campaign_id":null,"sandbox":false,"channel":"sms","phone":"242066000000","text":"Bonjour depuis mon application","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"sent","failure_reason":null,"price":"30.0000","currency":"XAF","scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":"2026-10-07T09:15:04.000000Z","created_at":"2026-10-07T09:15:00.000000Z"}},"echec":{"summary":"SMS en échec (prestataire masqué)","value":{"id":44,"campaign_id":null,"sandbox":false,"channel":"sms","phone":"242069999999","text":"Bonjour depuis mon application","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"failed","failure_reason":"SMS Gateway delivery report: INV (Invalid destination address)","price":"30.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":null,"created_at":"2026-10-07T09:15:00.000000Z"}},"whatsappEnvoye":{"summary":"WhatsApp envoyé","value":{"id":45,"campaign_id":null,"sandbox":false,"channel":"whatsapp","phone":"242066000000","text":"Bonjour sur WhatsApp","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"WhatsApp","status":"sent","failure_reason":null,"price":"2.5000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":"2026-10-07T09:15:20.000000Z","created_at":"2026-10-07T09:15:00.000000Z"}},"sandbox":{"summary":"Message sandbox","value":{"id":50,"campaign_id":null,"sandbox":true,"channel":"sms","phone":"242066000001","text":"Hello","media_url":null,"media_type":null,"interactive_type":null,"interactive_payload":null,"from":"MaMarque","status":"sent","failure_reason":null,"price":"0.0000","currency":null,"scheduled_at":"2026-10-07T09:15:00.000000Z","deferred_until":null,"sent_at":"2026-10-07T09:15:00.000000Z","created_at":"2026-10-07T09:15:00.000000Z"}}}}}},"responses":{"200":{"description":"Webhook reçu. Tout code 2xx est accepté."},"default":{"description":"Toute autre réponse (3xx, 4xx, 5xx) (ou absence de réponse sous 10 s) est un échec et entraîne une nouvelle tentative."}},"parameters":[{"name":"X-Gesflow-Event","in":"header","required":true,"description":"Type d’événement.","schema":{"type":"string","enum":["message.status","message.received"]},"example":"message.status"},{"name":"X-Gesflow-Delivery","in":"header","required":true,"description":"Identifiant unique du webhook, identique d’une tentative à l’autre.","schema":{"type":"string","format":"uuid"}},{"name":"X-Gesflow-Timestamp","in":"header","required":true,"description":"Horodatage Unix (secondes) de l’envoi.","schema":{"type":"integer"}},{"name":"X-Gesflow-Signature","in":"header","required":true,"description":"`sha256=` suivi du HMAC-SHA256 hexadécimal de `{X-Gesflow-Timestamp}.{corps brut}`, calculé avec le secret de signature du compte.","schema":{"type":"string"},"example":"sha256=5d2c7f0e9b1a…"},{"name":"X-Gesflow-Previous-Signature","in":"header","required":false,"description":"Présent pendant les 24 heures qui suivent une régénération du secret : la même signature, calculée avec l'ancien secret. Acceptez l'une ou l'autre le temps de mettre vos serveurs à jour.","schema":{"type":"string"},"example":"sha256=91be04c37d5a…"}]}},"inboundMessage":{"post":{"tags":["Messages entrants"],"operationId":"inboundMessageWebhook","summary":"Message reçu","description":"Envoyé à l'URL de webhook entrant configurée dans **Compte → Entreprise** (URL HTTPS obligatoire), à chaque SMS de réponse ou message WhatsApp reçu par le compte. Les doublons signalés par l'opérateur ou WhatsApp sont dédupliqués avant envoi. Les messages WhatsApp de groupes, de diffusion et de chaînes sont ignorés. L'URL est lue au moment de l'envoi : si elle est supprimée entre-temps, rien n'est envoyé.\n\n**Livraison** : requête `POST` JSON, en-têtes `Content-Type: application/json` et `Accept: application/json`. Chaque requête est **signée** (en-têtes `X-Gesflow-*`, voir « Webhooks » dans l’introduction) : vérifiez la signature avant de traiter le corps. Délai maximal de réponse : **10 secondes**. Seule une réponse **2xx** est un succès (les redirections ne sont pas suivies) (le corps de la réponse est ignoré). Une réponse 4xx/5xx, un timeout ou une erreur réseau déclenche une nouvelle tentative : **3 tentatives au total**, espacées de **60 secondes**. Si toutes échouent, les propriétaires du compte reçoivent un e-mail d'alerte (limité par compte et par URL).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboundMessageWebhookPayload"},"examples":{"sms":{"summary":"Réponse SMS","value":{"event":"message.received","data":{"id":128,"channel":"sms","phone":"242066000000","to":"MaMarque","text":"Oui, je confirme ma réservation","media_url":null,"media_mimetype":null,"received_at":"2026-10-06T09:15:00.000000Z","read_at":null}}},"whatsapp":{"summary":"Message WhatsApp avec image","value":{"event":"message.received","data":{"id":129,"channel":"whatsapp","phone":"242066000000","to":null,"text":"Voici la photo du colis","media_url":"https://waha.example.com/api/files/default/3EB0C767D26A1D.jpeg","media_mimetype":"image/jpeg","received_at":"2026-10-06T09:20:00.000000Z","read_at":null}}}}}}},"responses":{"200":{"description":"Webhook reçu. Tout code 2xx est accepté."},"default":{"description":"Toute autre réponse (3xx, 4xx, 5xx) (ou absence de réponse sous 10 s) est un échec et entraîne une nouvelle tentative."}},"parameters":[{"name":"X-Gesflow-Event","in":"header","required":true,"description":"Type d’événement.","schema":{"type":"string","enum":["message.status","message.received"]},"example":"message.received"},{"name":"X-Gesflow-Delivery","in":"header","required":true,"description":"Identifiant unique du webhook, identique d’une tentative à l’autre.","schema":{"type":"string","format":"uuid"}},{"name":"X-Gesflow-Timestamp","in":"header","required":true,"description":"Horodatage Unix (secondes) de l’envoi.","schema":{"type":"integer"}},{"name":"X-Gesflow-Signature","in":"header","required":true,"description":"`sha256=` suivi du HMAC-SHA256 hexadécimal de `{X-Gesflow-Timestamp}.{corps brut}`, calculé avec le secret de signature du compte.","schema":{"type":"string"},"example":"sha256=5d2c7f0e9b1a…"},{"name":"X-Gesflow-Previous-Signature","in":"header","required":false,"description":"Présent pendant les 24 heures qui suivent une régénération du secret : la même signature, calculée avec l'ancien secret. Acceptez l'une ou l'autre le temps de mettre vos serveurs à jour.","schema":{"type":"string"},"example":"sha256=91be04c37d5a…"}]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Clé API créée dans **Compte → Clés API** (format `<id>|<secret>`), envoyée dans `Authorization: Bearer <clé>`. Clé de production (envois réels) ou de test / sandbox (envois simulés)."}},"schemas":{"MessageChannel":{"type":"string","enum":["sms","whatsapp"],"description":"Canal de livraison.\n\n- `sms` : SMS via la passerelle opérateur (canal par défaut).\n- `whatsapp` : message WhatsApp envoyé depuis le numéro appairé au compte.","example":"sms"},"MessageStatus":{"type":"string","enum":["scheduled","processing","sent","failed"],"description":"Statut d'un message.\n\n- `scheduled` (Planifié) : envoi programmé (`scheduled_at` dans le futur), en attente de la date.\n- `processing` (En cours) : en file d'attente ou en cours de transmission à la passerelle SMS / WhatsApp (y compris un message WhatsApp immédiat étalé par l'anti-blocage, voir `deferred_until` ; un message programmé étalé reste `scheduled` jusqu'à son envoi effectif).\n- `sent` (Envoyé) : accepté par la passerelle SMS ou par WhatsApp. Ce statut ne garantit pas la réception : il peut encore passer à `failed` si l'opérateur ou WhatsApp signale ensuite un échec.\n- `failed` (Échoué) : échec définitif, raison dans `failure_reason`. Un envoi programmé annulé depuis l'application web passe aussi en `failed` (raison « Envoi programmé annulé par l'utilisateur. »).","example":"processing"},"WhatsappMediaType":{"type":"string","enum":["image","file","video","voice"],"description":"Type de média WhatsApp.\n\n- `image` : image (jpg, jpeg, png, gif, webp), `text` sert de légende.\n- `file` : document (pdf, doc, docx, xls, xlsx, csv, txt…), `text` sert de légende.\n- `video` : vidéo (mp4, mov, 3gp), `text` sert de légende.\n- `voice` : message vocal (ogg, opus, mp3, m4a, wav) ; `text` n'est **pas** transmis.\n\nLe type MIME est déduit de l'extension de l'URL, sinon une valeur par défaut est utilisée par type.","example":"image"},"WhatsappInteractiveType":{"type":"string","enum":["location","contact"],"description":"Contenu spécial d'un message WhatsApp, déduit des champs envoyés.\n\n- `location` : position géographique (`latitude`, `longitude`, `title`).\n- `contact` : une ou plusieurs fiches contact (`contacts`).","example":"location"},"PaymentStatus":{"type":"string","enum":["pending","processing","success","failed"],"description":"Statut d'un paiement Mobile Money.\n\n- `pending` (En attente) : paiement enregistré, pas encore transmis au prestataire.\n- `processing` (En cours) : demande transmise, en attente de la validation par le payeur sur son téléphone (USSD).\n- `success` (Réussi) : paiement confirmé, `base_amount` a été crédité sur le solde.\n- `failed` (Échoué) : paiement refusé, annulé ou en erreur (raison dans `failure_message`). Rien n'est crédité.","example":"processing"},"OtpCheckStatus":{"type":"string","enum":["approved","invalid","expired","max_attempts_reached","not_found"],"description":"Résultat de la vérification d'un code OTP.\n\n- `approved` : code correct, numéro vérifié. Un code ne peut être approuvé qu'une fois.\n- `invalid` : code incorrect ; une tentative est consommée.\n- `expired` : le dernier code envoyé a expiré ; demandez-en un nouveau.\n- `max_attempts_reached` : le nombre maximal de tentatives (5) est atteint pour ce code.\n- `not_found` : aucun code en attente pour ce numéro (jamais envoyé, déjà approuvé, ou envoyé avec un autre type de clé sandbox/production).","example":"approved"},"Message":{"type":"object","description":"Message sortant (SMS ou WhatsApp).","required":["id","campaign_id","sandbox","channel","phone","text","media_url","media_type","interactive_type","interactive_payload","from","status","failure_reason","price","currency","scheduled_at","deferred_until","sent_at","created_at"],"properties":{"id":{"type":"integer","description":"Identifiant du message.","example":42},"campaign_id":{"type":["integer","null"],"description":"Campagne à laquelle appartient le message (envoi en masse en production), sinon `null`.","example":null},"sandbox":{"type":"boolean","description":"`true` si le message a été simulé avec une clé sandbox.","example":false},"channel":{"allOf":[{"$ref":"#/components/schemas/MessageChannel"}],"description":"Canal du message : `sms` ou `whatsapp`. Toujours renseigné, y compris dans la réponse de création (`POST /messages`, `POST /messages/bulk`)."},"phone":{"type":"string","description":"Numéro du destinataire (chiffres uniquement).","example":"242066000000"},"text":{"type":"string","description":"Texte envoyé, après remplacement des variables de personnalisation. Chaîne vide pour un message WhatsApp sans texte (média, position ou contact seul).","example":"Bonjour depuis mon application"},"media_url":{"type":["string","null"],"format":"uri","description":"URL du média WhatsApp envoyé.","example":null},"media_type":{"anyOf":[{"$ref":"#/components/schemas/WhatsappMediaType"},{"type":"null"}],"description":"Type du média WhatsApp envoyé."},"interactive_type":{"anyOf":[{"$ref":"#/components/schemas/WhatsappInteractiveType"},{"type":"null"}],"description":"Type de contenu spécial WhatsApp."},"interactive_payload":{"description":"Contenu spécial WhatsApp normalisé : `{latitude, longitude, title}` pour `location`, `{contacts: [...]}` pour `contact`.","anyOf":[{"$ref":"#/components/schemas/LocationPayload"},{"$ref":"#/components/schemas/ContactPayload"},{"type":"null"}]},"from":{"type":"string","description":"Expéditeur : Sender ID utilisé pour un SMS, `WhatsApp` pour un message WhatsApp.","example":"MaMarque"},"status":{"$ref":"#/components/schemas/MessageStatus"},"failure_reason":{"type":["string","null"],"description":"Raison de l'échec lorsque `status` vaut `failed`. Texte libre (peut contenir le code opérateur, ex. `Wirepick delivery report: INV (Invalid destination address)`). Dans les webhooks, les noms des prestataires sont remplacés par « SMS Gateway » / « WhatsApp Gateway ».","example":null},"price":{"type":"string","pattern":"^-?[0-9]+\\.[0-9]{4}$","description":"Prix débité pour ce message, en FCFA, chaîne à 4 décimales. Pour un SMS, ajusté au nombre de segments une fois envoyé. `\"0.0000\"` en sandbox.","example":"30.0000"},"currency":{"type":["string","null"],"description":"Devise renvoyée par la passerelle SMS une fois le SMS envoyé ; `null` avant l'envoi, pour WhatsApp et en sandbox.","example":null},"scheduled_at":{"type":["string","null"],"format":"date-time","description":"Date d'envoi prévue (UTC). Vaut la date de création pour un envoi immédiat.","example":"2026-10-07T09:15:00.000000Z"},"deferred_until":{"type":["string","null"],"format":"date-time","description":"WhatsApp uniquement : heure prévue d'envoi lorsque l'anti-blocage a étalé le message ; `null` sinon (remis à `null` une fois envoyé).","example":null},"sent_at":{"type":["string","null"],"format":"date-time","description":"Date à laquelle le message a été accepté par la passerelle / WhatsApp.","example":null},"replayed_at":{"type":["string","null"],"format":"date-time","description":"Date à laquelle ce message échoué a été renvoyé depuis le portail (sous la forme d'un nouveau message) ; `null` sinon.","example":null},"replayable":{"type":"boolean","description":"`true` si ce message échoué peut encore être renvoyé depuis le portail.","example":false},"created_at":{"type":["string","null"],"format":"date-time","description":"Date de création.","example":"2026-10-07T09:15:00.000000Z"}}},"LocationPayload":{"type":"object","title":"Position","required":["latitude","longitude","title"],"properties":{"latitude":{"type":"number","example":-4.2634},"longitude":{"type":"number","example":15.2429},"title":{"type":"string","example":"Agence Centre-ville"}}},"ContactCard":{"type":"object","description":"Fiche contact WhatsApp. Fournissez soit `vcard`, soit `full_name` **et** `phone_number` (sinon erreur 422 sur `contacts.N`). Si `vcard` est fourni, les autres champs sont ignorés à l'envoi.","properties":{"vcard":{"type":["string","null"],"description":"Fiche vCard brute (format vCard 3.0).","example":"BEGIN:VCARD\nVERSION:3.0\nFN:Service client\nTEL;type=CELL;waid=242066000099:+242 06 600 00 99\nEND:VCARD"},"full_name":{"type":["string","null"],"maxLength":100,"example":"Service client"},"phone_number":{"type":["string","null"],"maxLength":20,"example":"+242066000099"},"organization":{"type":["string","null"],"maxLength":100,"example":"MaMarque"},"whatsapp_id":{"type":["string","null"],"maxLength":20,"description":"Identifiant WhatsApp (numéro sans +).","example":"242066000099"}}},"ContactPayload":{"type":"object","title":"Fiches contact","required":["contacts"],"properties":{"contacts":{"type":"array","items":{"$ref":"#/components/schemas/ContactCard"}}}},"PaginationLinks":{"type":"object","description":"Liens de navigation (seul le paramètre `page` est conservé dans les URL).","required":["first","last","prev","next"],"properties":{"first":{"type":["string","null"],"format":"uri","example":"https://sender.gesflow.biz/api/v1/messages?page=1"},"last":{"type":["string","null"],"format":"uri","example":"https://sender.gesflow.biz/api/v1/messages?page=5"},"prev":{"type":["string","null"],"format":"uri","example":null},"next":{"type":["string","null"],"format":"uri","example":"https://sender.gesflow.biz/api/v1/messages?page=2"}}},"PaginationMetaLink":{"type":"object","required":["url","label","active"],"properties":{"url":{"type":["string","null"],"format":"uri","example":"https://sender.gesflow.biz/api/v1/messages?page=2"},"label":{"type":"string","description":"Libellé (numéro de page, « &laquo; Précédent », « Suivant &raquo; » ou « ... »).","example":"2"},"page":{"type":["integer","null"],"description":"Numéro de page cible (absent pour le séparateur « ... »).","example":2},"active":{"type":"boolean","example":false}}},"PaginationMeta":{"type":"object","required":["current_page","from","last_page","links","path","per_page","to","total"],"properties":{"current_page":{"type":"integer","example":1},"from":{"type":["integer","null"],"description":"Rang du premier élément de la page (`null` si la page est vide).","example":1},"last_page":{"type":"integer","example":5},"links":{"type":"array","items":{"$ref":"#/components/schemas/PaginationMetaLink"}},"path":{"type":"string","format":"uri","example":"https://sender.gesflow.biz/api/v1/messages"},"per_page":{"type":"integer","example":25},"to":{"type":["integer","null"],"description":"Rang du dernier élément de la page (`null` si la page est vide).","example":25},"total":{"type":"integer","example":112}}},"MessageResponse":{"type":"object","description":"Un message.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Message"}}},"MessageCollection":{"type":"object","description":"Liste paginée de messages (25 par page).","required":["data","links","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"links":{"$ref":"#/components/schemas/PaginationLinks"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"BulkMessageResponse":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Message"},"description":"Un message créé par destinataire (après déduplication)."},"meta":{"type":"object","required":["count","skipped_rows","campaign_id"],"properties":{"count":{"type":"integer","description":"Nombre de messages créés.","example":2},"skipped_rows":{"type":"integer","description":"Nombre de lignes du fichier importé sans numéro exploitable (ligne d'en-tête comprise). `0` sans fichier.","example":0},"campaign_id":{"type":["integer","null"],"description":"Identifiant de la campagne créée (`null` en sandbox).","example":17}}}}},"CampaignStats":{"type":"object","description":"Statistiques calculées en temps réel sur les messages de la campagne.","required":["total","sent","failed","pending","scheduled","delivery_rate","failure_rate","cost"],"properties":{"total":{"type":"integer","description":"Nombre total de messages.","example":1250},"sent":{"type":"integer","description":"Messages au statut `sent`.","example":1198},"failed":{"type":"integer","description":"Messages au statut `failed`.","example":32},"pending":{"type":"integer","description":"Messages au statut `scheduled` ou `processing`.","example":20},"scheduled":{"type":"integer","description":"Messages au statut `scheduled` dont la date d'envoi est encore future.","example":0},"delivery_rate":{"type":"number","description":"`sent / total × 100`, arrondi à 1 décimale (0 si aucun message).","example":95.8},"failure_rate":{"type":"number","description":"`failed / total × 100`, arrondi à 1 décimale (0 si aucun message).","example":2.6},"cost":{"type":"number","description":"Somme des prix des messages non échoués, en FCFA, arrondie à 2 décimales.","example":36540}}},"Campaign":{"type":"object","description":"Campagne : groupe de messages créé par un envoi en masse (API ou application web).","required":["id","name","channel","text","from","recipients_count","scheduled_at","created_at","stats"],"properties":{"id":{"type":"integer","example":17},"name":{"type":"string","description":"Nom de la campagne (`campaign_name`, ou « Envoi en masse du JJ/MM/AAAA HH:MM » par défaut).","example":"Promo Black Friday"},"channel":{"$ref":"#/components/schemas/MessageChannel"},"text":{"type":"string","description":"Texte de la campagne, variables de personnalisation non remplacées.","example":"Bonjour {{ prenom|cher client }}, -20 % ce week-end !"},"from":{"type":["string","null"],"description":"Sender ID utilisé.","example":"MaMarque"},"recipients_count":{"type":"integer","description":"Nombre de destinataires au moment de l'envoi.","example":1250},"scheduled_at":{"type":["string","null"],"format":"date-time","description":"Date d'envoi (date de création pour un envoi immédiat).","example":"2026-10-06T08:00:00.000000Z"},"created_at":{"type":["string","null"],"format":"date-time","example":"2026-10-06T08:00:00.000000Z"},"stats":{"$ref":"#/components/schemas/CampaignStats"}}},"CampaignResponse":{"type":"object","description":"Une campagne.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Campaign"}}},"CampaignCollection":{"type":"object","description":"Liste paginée de campagnes (25 par page).","required":["data","links","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Campaign"}},"links":{"$ref":"#/components/schemas/PaginationLinks"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"InboundMessage":{"type":"object","description":"Message reçu : réponse SMS ou message WhatsApp entrant.","required":["id","channel","phone","to","text","media_url","media_mimetype","received_at","read_at"],"properties":{"id":{"type":"integer","example":128},"channel":{"$ref":"#/components/schemas/MessageChannel"},"phone":{"type":"string","description":"Numéro de l'expéditeur (chiffres).","example":"242066000000"},"to":{"type":["string","null"],"description":"SMS : Sender ID / numéro auquel la réponse a été envoyée (`null` si l'opérateur ne le transmet pas). WhatsApp : `null`.","example":"MaMarque"},"text":{"type":"string","description":"Contenu (chaîne vide possible pour un média WhatsApp sans légende).","example":"Oui, je confirme ma réservation"},"media_url":{"type":["string","null"],"format":"uri","description":"WhatsApp : URL du média reçu.","example":null},"media_mimetype":{"type":["string","null"],"description":"WhatsApp : type MIME du média reçu.","example":null},"received_at":{"type":["string","null"],"format":"date-time","example":"2026-10-06T09:15:00.000000Z"},"read_at":{"type":["string","null"],"format":"date-time","description":"Date de lecture dans la boîte de réception de l'application web.","example":null}}},"InboundMessageCollection":{"type":"object","description":"Liste paginée de messages reçus (25 par page).","required":["data","links","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InboundMessage"}},"links":{"$ref":"#/components/schemas/PaginationLinks"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"SenderId":{"type":"object","required":["id","value","is_default","created_at"],"properties":{"id":{"type":"integer","example":1},"value":{"type":"string","description":"Valeur à passer dans `sender_id`.","example":"MaMarque"},"is_default":{"type":"boolean","description":"Sender ID utilisé par défaut quand `sender_id` est omis.","example":true},"created_at":{"type":["string","null"],"format":"date-time","example":"2026-01-15T10:30:00.000000Z"}}},"SenderIdCollection":{"type":"object","description":"Liste non paginée des Sender IDs actifs (non suspendus), Sender ID par défaut en premier puis par ordre alphabétique.","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SenderId"}}}},"Payment":{"type":"object","description":"Recharge Mobile Money.","required":["id","reference","status","status_label","base_amount","service_fee","transaction_fee","amount","currency","phone_number","operator","country","failure_message","created_at","updated_at"],"properties":{"id":{"type":"integer","example":31},"reference":{"type":"string","description":"Référence unique `PAY-XXXXXXXX`.","example":"PAY-7QK2M9XA"},"status":{"$ref":"#/components/schemas/PaymentStatus"},"status_label":{"type":"string","enum":["En attente","En cours","Réussi","Échoué"],"description":"Libellé français du statut.","example":"En cours"},"base_amount":{"type":"string","description":"Montant crédité sur le solde en cas de succès (4 décimales).","example":"5000.0000"},"service_fee":{"type":"string","description":"Frais du prestataire de paiement : `(base_amount + transaction_fee) × provider_fee_percentage + provider_fee_fixed`.","example":"233.0750"},"transaction_fee":{"type":"string","description":"Taxes : `base_amount × tax_vat_percentage × (1 + tax_ca_percentage)`.","example":"945.0000"},"amount":{"type":"string","description":"Total facturé au payeur : `base_amount + service_fee + transaction_fee` (arrondi à l'unité lors de la demande au prestataire).","example":"6178.0750"},"currency":{"type":"string","example":"XAF"},"phone_number":{"type":"string","description":"Numéro Mobile Money normalisé (avec +).","example":"+242066000000"},"operator":{"type":"string","enum":["mtn","airtel"],"example":"mtn"},"country":{"type":"string","description":"Code pays ISO à 2 lettres.","example":"cg"},"failure_message":{"type":["string","null"],"description":"Raison de l'échec.","example":null},"created_at":{"type":["string","null"],"format":"date-time","description":"ISO 8601 avec décalage (`+00:00`).","example":"2026-10-07T09:15:00+00:00"},"updated_at":{"type":["string","null"],"format":"date-time","example":"2026-10-07T09:15:01+00:00"}}},"PaymentResponse":{"type":"object","description":"Un paiement.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Payment"}}},"PaymentCollection":{"type":"object","description":"Liste paginée des paiements du compte.","required":["data","links","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}},"links":{"$ref":"#/components/schemas/PaginationLinks"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"PaymentFeesConfig":{"type":"object","description":"Paramètres publics de calcul des frais d'une recharge.","required":["min_amount","provider_fee_percentage","provider_fee_fixed","tax_vat_percentage","tax_ca_percentage","country","currency","phone_country_code"],"properties":{"min_amount":{"type":"number","description":"Montant de base minimal d'une recharge.","example":500},"provider_fee_percentage":{"type":"number","description":"Frais variables du prestataire (fraction, 0.035 = 3,5 %), appliqués au montant TTC.","example":0.035},"provider_fee_fixed":{"type":"number","description":"Frais fixes du prestataire.","example":25},"tax_vat_percentage":{"type":"number","description":"TVA (fraction) appliquée au montant de base.","example":0.18},"tax_ca_percentage":{"type":"number","description":"Centimes additionnels (fraction) appliqués au montant de la TVA.","example":0.05},"country":{"type":"string","example":"cg"},"currency":{"type":"string","example":"XAF"},"phone_country_code":{"type":"string","description":"Indicatif ajouté aux numéros saisis sans indicatif.","example":"+242"}}},"OtpVerification":{"type":"object","required":["id","phone","channel","status","message_id","expires_at","sandbox"],"properties":{"id":{"type":"string","description":"Identifiant public de la vérification (ULID).","example":"01K6Z3X8T9Q4M2N7B5V1C0D8EF"},"phone":{"type":"string","description":"Numéro normalisé.","example":"242066000000"},"channel":{"$ref":"#/components/schemas/MessageChannel"},"status":{"type":"string","enum":["pending"],"description":"Toujours `pending` à l'envoi.","example":"pending"},"message_id":{"type":["integer","null"],"description":"Identifiant du message porteur du code (consultable via `GET /messages/{message}` avec le même type de clé).","example":512},"expires_at":{"type":"string","format":"date-time","example":"2026-10-07T09:20:00.000000Z"},"sandbox":{"type":"boolean","example":false},"code":{"type":"string","pattern":"^[0-9]{4,8}$","description":"**Uniquement avec une clé sandbox** : le code généré.","example":"482913"}}},"OtpVerificationResponse":{"type":"object","description":"Code OTP envoyé.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OtpVerification"}}},"OtpCheckResult":{"type":"object","required":["id","phone","valid","status","attempts_remaining"],"properties":{"id":{"type":["string","null"],"description":"Identifiant de la vérification concernée (`null` si `not_found`).","example":"01K6Z3X8T9Q4M2N7B5V1C0D8EF"},"phone":{"type":"string","example":"242066000000"},"valid":{"type":"boolean","description":"`true` uniquement si `status` vaut `approved`.","example":true},"status":{"$ref":"#/components/schemas/OtpCheckStatus"},"attempts_remaining":{"type":"integer","description":"Tentatives restantes pour ce code (0 si `not_found`).","example":4}}},"OtpCheckResponse":{"type":"object","description":"Résultat de la vérification.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OtpCheckResult"}}},"WhatsappNumberCheck":{"type":"object","required":["phone","has_whatsapp"],"properties":{"phone":{"type":"string","example":"242066000000"},"has_whatsapp":{"type":"boolean","example":true}}},"WhatsappNumberCheckResponse":{"type":"object","description":"Résultat de la vérification WhatsApp.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WhatsappNumberCheck"}}},"Error":{"type":"object","description":"Erreur générique ou erreur métier.","required":["message"],"properties":{"message":{"type":"string","example":"Solde de crédits insuffisant pour envoyer ce message."}}},"ValidationError":{"type":"object","description":"Erreur de validation (422). `errors` associe chaque champ (notation pointée pour les tableaux, ex. `phones.1`, `contacts.0`) à la liste de ses messages.","required":["message","errors"],"properties":{"message":{"type":"string","description":"Premier message d'erreur, suivi de « (and N more error(s)) » s'il y en a d'autres.","example":"Le champ numéro de téléphone est obligatoire. (and 1 more error)"},"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"example":{"phone":["Le champ numéro de téléphone est obligatoire."],"text":["Le champ message est obligatoire."]}}}},"OtpThrottledError":{"type":"object","required":["message","retry_after"],"properties":{"message":{"type":"string","example":"Trop de codes envoyés à ce numéro. Réessayez dans 24 secondes."},"retry_after":{"type":"integer","description":"Délai d'attente en secondes (identique à l'en-tête `Retry-After`).","example":24}}},"SendMessageRequest":{"type":"object","required":["phone"],"description":"Corps d'envoi d'un message. Règles :\n\n- `text` est obligatoire, sauf pour WhatsApp quand `media_url`, `latitude` ou `contacts` est fourni.\n- `media_url`, `latitude` et `contacts` sont **interdits** sur le canal `sms` (erreur « Le champ … n'est pas autorisé »).\n- Un seul type de contenu spécial par message (média **ou** position **ou** contacts), sinon erreur 422 sur la clé `channel`.\n- Le texte peut contenir des variables de personnalisation (`{{ prenom|cher client }}`).","properties":{"channel":{"allOf":[{"$ref":"#/components/schemas/MessageChannel"}],"default":"sms","description":"Canal (`sms` par défaut)."},"phone":{"type":"string","description":"Numéro du destinataire, 8 à 15 chiffres après suppression des caractères non numériques.","example":"242066000000"},"text":{"type":["string","null"],"maxLength":1600,"description":"Texte du message (1600 caractères max). Pour un média image/vidéo/fichier, sert de légende.","example":"Bonjour depuis mon application"},"scheduled_at":{"type":["string","null"],"format":"date-time","description":"Date/heure d'envoi programmé, strictement dans le futur (règle `after:now`). Omis : envoi immédiat. Le prix est débité dès maintenant.","example":"2026-10-08T08:00:00Z"},"callback_url":{"type":["string","null"],"format":"uri","maxLength":2048,"description":"URL appelée en POST à chaque changement de statut (voir webhook `messageStatus`). HTTP ou HTTPS. Doit viser une adresse publique (jamais `localhost` ni une adresse IP privée).","example":"https://votre-domaine.com/webhooks/sms-status"},"flash":{"type":"boolean","default":false,"description":"SMS flash (affiché directement à l'écran). Accepte `true`, `false`, `1`, `0`, `\"1\"`, `\"0\"`. Ignoré pour WhatsApp.","example":false},"sender_id":{"type":["string","null"],"description":"Sender ID à utiliser : doit être un Sender ID **actif** (non suspendu) du compte, sinon 422 « La valeur de Sender ID est invalide. » — y compris pour WhatsApp, où il est ensuite ignoré. Omis : Sender ID par défaut, ou l'unique Sender ID actif ; sinon erreur métier 422.","example":"MaMarque"},"media_url":{"type":["string","null"],"format":"uri","maxLength":2048,"description":"WhatsApp uniquement. URL publique du média. Requiert `media_type`.","example":"https://cdn.example.com/catalogue/automne.jpg"},"media_type":{"anyOf":[{"$ref":"#/components/schemas/WhatsappMediaType"},{"type":"null"}],"description":"Requis avec `media_url` (et inversement)."},"latitude":{"type":["number","null"],"minimum":-90,"maximum":90,"description":"WhatsApp uniquement. Requiert `longitude` et `title`.","example":-4.2634},"longitude":{"type":["number","null"],"minimum":-180,"maximum":180,"description":"Requiert `latitude` et `title`.","example":15.2429},"title":{"type":["string","null"],"maxLength":100,"description":"Nom du lieu. Requiert `latitude` et `longitude`.","example":"Agence Centre-ville"},"contacts":{"type":["array","null"],"minItems":1,"maxItems":10,"items":{"$ref":"#/components/schemas/ContactCard"},"description":"WhatsApp uniquement. 1 à 10 fiches contact."}}},"SendBulkMessageRequest":{"type":"object","required":[],"description":"Envoi en masse. Fournissez au moins une source de destinataires : `phones`, `file`, `group_ids` ou `tags` (combinables). Les numéros de toutes les sources sont fusionnés et dédupliqués ; le total est limité à **10 000 destinataires en SMS** et **500 sur WhatsApp**.","properties":{"channel":{"type":"string","enum":["sms","whatsapp"],"default":"sms","description":"Canal de l’envoi. WhatsApp : numéro appairé requis (sauf sandbox), `sender_id` et `flash` ignorés.","example":"sms"},"phones":{"type":"array","maxItems":10000,"items":{"type":"string","description":"8 à 15 chiffres après nettoyage."},"description":"Numéros destinataires (10 000 max en SMS, 500 sur WhatsApp, avant déduplication). Requis si ni `file`, ni `group_ids`, ni `tags`.","example":["242066000001","242066000002"]},"group_ids":{"type":["array","null"],"items":{"type":"integer"},"description":"Identifiants de groupes de contacts **de votre compte** : tous leurs contacts sont ajoutés.","example":[4]},"tags":{"type":["array","null"],"items":{"type":"string","maxLength":50},"description":"Tags de contacts : tous les contacts portant au moins un de ces tags sont ajoutés.","example":["vip"]},"campaign_name":{"type":["string","null"],"maxLength":255,"description":"Nom de la campagne créée. Par défaut « Envoi en masse du JJ/MM/AAAA HH:MM ».","example":"Promo Black Friday"},"text":{"type":["string","null"],"maxLength":1600,"description":"Texte commun, avec variables de personnalisation résolues pour chaque destinataire. Requis, sauf pour un envoi WhatsApp avec média (il sert alors de légende).","example":"Bonjour {{ prenom|cher client }}, -20 % ce week-end !"},"scheduled_at":{"type":["string","null"],"format":"date-time","description":"Envoi programmé, strictement dans le futur.","example":"2026-11-27T07:00:00Z"},"callback_url":{"type":["string","null"],"format":"uri","maxLength":2048,"description":"Webhook de statut, appliqué à chaque message. Doit viser une adresse publique (voir « Webhooks »).","example":"https://votre-domaine.com/webhooks/sms-status"},"flash":{"type":"boolean","default":false,"example":false},"sender_id":{"type":["string","null"],"description":"Sender ID actif du compte (mêmes règles que `POST /messages`).","example":"MaMarque"},"media_url":{"type":["string","null"],"format":"uri","maxLength":2048,"description":"WhatsApp uniquement : URL publique du média envoyé à chaque destinataire. Requiert `media_type`.","example":"https://cdn.votre-domaine.com/catalogue.pdf"},"media_type":{"oneOf":[{"$ref":"#/components/schemas/WhatsappMediaType"},{"type":"null"}],"description":"WhatsApp uniquement : type du média. Requiert `media_url`."}}},"SendBulkMessageMultipartRequest":{"type":"object","required":["text"],"description":"Variante `multipart/form-data` permettant d'importer un fichier de contacts. Les tableaux s'envoient sous la forme `phones[]=...`, `group_ids[]=...`, `tags[]=...`. Pour `flash`, utilisez `1`/`0` (les chaînes `true`/`false` sont refusées en multipart).","properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","description":"Fichier CSV, TXT ou XLSX (10 Mo max). Sur chaque ligne, la première cellule ressemblant à un numéro (8 à 15 chiffres une fois nettoyée) est retenue ; les autres lignes (dont l'en-tête) sont comptées dans `meta.skipped_rows`. Séparateur CSV détecté automatiquement (`;`, `,` ou tabulation)."},"phones[]":{"type":"array","items":{"type":"string"}},"group_ids[]":{"type":"array","items":{"type":"integer"}},"tags[]":{"type":"array","items":{"type":"string"}},"campaign_name":{"type":"string","maxLength":255},"text":{"type":"string","maxLength":1600},"scheduled_at":{"type":"string","format":"date-time"},"callback_url":{"type":"string","format":"uri","maxLength":2048},"flash":{"type":"string","enum":["0","1"]},"sender_id":{"type":"string"}}},"SendOtpRequest":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string","description":"Numéro à vérifier (8 à 15 chiffres après nettoyage).","example":"242066000000"},"channel":{"allOf":[{"$ref":"#/components/schemas/MessageChannel"}],"default":"sms","description":"Canal d'envoi du code. WhatsApp exige une session connectée (hors sandbox)."},"code_length":{"type":"integer","minimum":4,"maximum":8,"default":6,"description":"Nombre de chiffres du code.","example":6},"expires_in":{"type":"integer","minimum":60,"maximum":3600,"default":300,"description":"Durée de validité en secondes.","example":300},"template":{"type":["string","null"],"maxLength":300,"description":"Texte du message ; doit contenir la variable du code `{{ code }}` et peut contenir `{{ minutes }}` (durée de validité en minutes, minimum 1). Les espaces à l'intérieur des accolades sont libres (`{{code}}`, `{{ code }}`…). Les variables de personnalisation des contacts (`{{ prenom|... }}`) sont aussi disponibles. Par défaut : « Votre code de vérification est {{ code }}. Il expire dans {{ minutes }} minutes. »","example":"MyApp : votre code est {{ code }} ({{ minutes }} min)."},"sender_id":{"type":["string","null"],"description":"Sender ID actif du compte (SMS uniquement ; validé mais ignoré pour WhatsApp).","example":"MaMarque"}}},"CheckOtpRequest":{"type":"object","required":["phone","code"],"properties":{"phone":{"type":"string","description":"Numéro auquel le code a été envoyé (8 à 15 chiffres après nettoyage).","example":"242066000000"},"code":{"type":"string","pattern":"^[0-9]{4,8}$","description":"Code saisi par l'utilisateur (4 à 8 chiffres).","example":"482913"}}},"InitiatePaymentRequest":{"type":"object","required":["amount","phone_number","operator"],"properties":{"amount":{"type":"number","description":"Montant de base à créditer (FCFA), au moins `min_amount` (voir `GET /payments/fees-config`). Les frais et taxes s'y ajoutent.","example":5000},"phone_number":{"type":"string","description":"Numéro Mobile Money du payeur. Normalisé : chiffres uniquement, puis `+` ajouté si le numéro commence par `+` ou par l'indicatif (242), sinon l'indicatif `+242` est ajouté. Doit ensuite correspondre à `^\\+[0-9]{8,15}$`.","example":"066000000"},"operator":{"type":"string","enum":["mtn","airtel"],"description":"Opérateur Mobile Money, en minuscules (`MTN` ou `Airtel` sont refusés en 422).","example":"mtn"}}},"MessageStatusWebhookPayload":{"allOf":[{"$ref":"#/components/schemas/Message"}],"description":"Corps envoyé à `callback_url` : l'objet message complet (non enveloppé dans `data`), dans l'état où il se trouve **au moment de l'envoi du webhook**. Dans `failure_reason`, les noms de prestataires sont masqués (« wirepick » et « yabetoo » → « SMS Gateway », « waha » → « WhatsApp Gateway », sans tenir compte de la casse)."},"InboundMessageWebhookPayload":{"type":"object","required":["event","data"],"properties":{"event":{"type":"string","enum":["message.received"],"example":"message.received"},"data":{"$ref":"#/components/schemas/InboundMessage"}}},"CreditTransactionType":{"type":"string","description":"Nature du mouvement : `opening_balance` (solde à l'ouverture de l'historique), `recharge` (recharge par un administrateur), `payment` (recharge Mobile Money), `message_charge` (envoi d'un message), `campaign_charge` (envoi en masse, débité en une fois), `message_adjustment` (SMS long facturé en plusieurs parties), `message_refund` (message non délivré remboursé), `cancellation_refund` (envoi programmé annulé), `adjustment` (ajustement par un administrateur : geste commercial ou correction, au crédit ou au débit).","enum":["opening_balance","recharge","payment","message_charge","campaign_charge","message_adjustment","message_refund","cancellation_refund","adjustment"]},"Account":{"type":"object","required":["id","name","credit_balance","currency","prices","remaining"],"properties":{"id":{"type":"integer","example":12},"name":{"type":"string","example":"Boutique Mbote"},"credit_balance":{"type":"string","description":"Solde de crédits du compte, en FCFA.","example":"12500.0000"},"currency":{"type":"string","enum":["XAF"],"example":"XAF"},"prices":{"type":"object","description":"Prix unitaires appliqués au compte.","required":["sms","whatsapp"],"properties":{"sms":{"type":"string","description":"Prix d’un SMS (segment) pour ce compte.","example":"20.0000"},"whatsapp":{"type":"string","description":"Prix d’un message WhatsApp.","example":"2.5000"}}},"remaining":{"type":"object","description":"Nombre de messages que le solde couvre encore, par canal (`null` si le prix est nul).","required":["sms","whatsapp"],"properties":{"sms":{"type":["integer","null"],"example":625},"whatsapp":{"type":["integer","null"],"example":5000}}}}},"AccountResponse":{"type":"object","description":"Le compte.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Account"}}},"CreditTransaction":{"type":"object","required":["id","type","label","amount","balance_after","description","reference","created_at"],"properties":{"id":{"type":"integer","example":981},"type":{"$ref":"#/components/schemas/CreditTransactionType"},"label":{"type":"string","description":"Libellé français du type.","example":"Envoi de message"},"amount":{"type":"string","description":"Montant du mouvement en FCFA : positif pour un crédit, négatif pour un débit.","example":"-20.0000"},"balance_after":{"type":"string","description":"Solde du compte juste après ce mouvement.","example":"12480.0000"},"description":{"type":["string","null"],"example":"SMS vers 242066000001"},"reference":{"type":["object","null"],"description":"Objet concerné par le mouvement, s’il y en a un.","required":["type","id"],"properties":{"type":{"type":"string","enum":["message","campaign","account_recharge","payment"],"example":"message"},"id":{"type":"integer","example":42}}},"created_at":{"type":"string","format":"date-time","example":"2026-10-07T09:15:00.000000Z"}}},"CreditTransactionCollection":{"type":"object","description":"Mouvements du solde, du plus récent au plus ancien (25 par page).","required":["data","links","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CreditTransaction"}},"links":{"$ref":"#/components/schemas/PaginationLinks"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"ContactGroup":{"type":"object","required":["id","name","description","created_at"],"properties":{"id":{"type":"integer","example":4},"name":{"type":"string","example":"Clients VIP"},"description":{"type":["string","null"],"example":"Meilleurs clients de l’année"},"contacts_count":{"type":"integer","description":"Nombre de contacts du groupe (absent dans la liste `groups` d’un contact).","example":128},"created_at":{"type":"string","format":"date-time","example":"2026-09-01T10:00:00.000000Z"}}},"ContactGroupResponse":{"type":"object","description":"Un groupe de contacts.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ContactGroup"}}},"ContactGroupCollection":{"type":"object","description":"Groupes de contacts, par nom (25 par page).","required":["data","links","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ContactGroup"}},"links":{"$ref":"#/components/schemas/PaginationLinks"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"Contact":{"type":"object","required":["id","name","phone","tags","custom_fields","created_at","updated_at"],"properties":{"id":{"type":"integer","example":311},"name":{"type":"string","example":"Jean Dupont"},"phone":{"type":"string","example":"242066000001"},"tags":{"type":"array","items":{"type":"string"},"description":"Tags en minuscules.","example":["client","vip"]},"custom_fields":{"type":"object","additionalProperties":{"type":"string"},"description":"Champs personnalisés, utilisables comme variables (`{{ ville }}`).","example":{"ville":"Brazzaville"}},"groups":{"type":"array","items":{"$ref":"#/components/schemas/ContactGroup"},"description":"Groupes du contact (`id`, `name`, `description`, `created_at`)."},"created_at":{"type":"string","format":"date-time","example":"2026-09-01T10:00:00.000000Z"},"updated_at":{"type":"string","format":"date-time","example":"2026-09-15T08:30:00.000000Z"}}},"ContactResponse":{"type":"object","description":"Un contact.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Contact"}}},"ContactCollection":{"type":"object","description":"Contacts, du plus récent au plus ancien (25 par page).","required":["data","links","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"links":{"$ref":"#/components/schemas/PaginationLinks"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"ContactRequest":{"type":"object","required":["name","phone"],"properties":{"name":{"type":"string","maxLength":255,"example":"Jean Dupont"},"phone":{"type":"string","description":"Numéro international sans `+`, 8 à 15 chiffres, unique dans le compte.","example":"242066000001"},"tags":{"type":["array","null"],"maxItems":20,"items":{"type":"string","maxLength":50},"description":"Tags (mis en minuscules et dédupliqués). Une chaîne séparée par des virgules est aussi acceptée.","example":["client","vip"]},"custom_fields":{"type":["object","null"],"maxProperties":20,"additionalProperties":{"type":"string","maxLength":255},"description":"Champs personnalisés (clés converties en `snake_case`, valeurs vides ignorées).","example":{"ville":"Brazzaville"}},"group_ids":{"type":["array","null"],"items":{"type":"integer"},"description":"Groupes du compte auxquels rattacher le contact. En modification, les groupes ne changent que si ce champ est envoyé (tableau vide : retire de tous les groupes).","example":[4]}}},"ContactGroupRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":100,"description":"Unique dans le compte.","example":"Clients VIP"},"description":{"type":["string","null"],"maxLength":500,"example":"Meilleurs clients de l’année"}}},"ContactIdsRequest":{"type":"object","required":["contact_ids"],"properties":{"contact_ids":{"type":"array","minItems":1,"maxItems":1000,"items":{"type":"integer"},"description":"Identifiants de contacts **de votre compte**.","example":[311,312]}}},"Template":{"type":"object","required":["id","name","content","created_at","updated_at"],"properties":{"id":{"type":"integer","example":7},"name":{"type":"string","example":"Bienvenue"},"content":{"type":"string","example":"Bonjour {{ prenom|cher client }}, bienvenue chez nous !"},"created_at":{"type":"string","format":"date-time","example":"2026-09-01T10:00:00.000000Z"},"updated_at":{"type":"string","format":"date-time","example":"2026-09-01T10:00:00.000000Z"}}},"TemplateResponse":{"type":"object","description":"Un modèle.","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Template"}}},"TemplateCollection":{"type":"object","description":"Modèles, du plus récent au plus ancien (25 par page).","required":["data","links","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Template"}},"links":{"$ref":"#/components/schemas/PaginationLinks"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"TemplateRequest":{"type":"object","required":["name","content"],"properties":{"name":{"type":"string","maxLength":150,"description":"Unique dans le compte.","example":"Bienvenue"},"content":{"type":"string","maxLength":1600,"description":"Texte du modèle, avec variables de personnalisation.","example":"Bonjour {{ prenom|cher client }}, bienvenue chez nous !"}}},"CampaignCancelResponse":{"type":"object","description":"La campagne après annulation, et le nombre de messages annulés.","required":["data","meta"],"properties":{"data":{"$ref":"#/components/schemas/Campaign"},"meta":{"type":"object","required":["cancelled"],"properties":{"cancelled":{"type":"integer","description":"Messages programmés annulés et remboursés.","example":1250}}}}}},"responses":{"Unauthenticated":{"description":"Clé API absente, invalide, expirée ou révoquée.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthenticated":{"summary":"Non authentifié","value":{"message":"Unauthenticated."}}}}}},"AccountSuspended":{"description":"Compte suspendu par l'équipe GESFLOW SENDER (fraude, abus, impayé…) : aucun envoi n'est accepté jusqu'à sa réactivation. Les autres appels (lecture des messages, du solde…) restent possibles.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"suspended":{"summary":"Compte suspendu","value":{"message":"Votre compte est suspendu : aucun envoi n’est possible. Contactez le support pour en savoir plus."}}}}}},"MessageNotFound":{"description":"Message inexistant, appartenant à un autre compte, ou d'un autre type que la clé (sandbox / production). Un identifiant non numérique (ex. `/messages/abc`) ne correspond à aucune route : 404 également, avec un `message` différent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"notFound":{"summary":"Message introuvable","value":{"message":"No query results for model [App\\Models\\Message] 42"}},"nonNumeric":{"summary":"Identifiant non numérique","value":{"message":"The route api/v1/messages/abc could not be found."}}}}}},"CampaignNotFound":{"description":"Campagne inexistante ou appartenant à un autre compte. Un identifiant non numérique (ex. `/campaigns/abc`) ne correspond à aucune route : 404 également, avec un `message` différent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"notFound":{"summary":"Campagne introuvable","value":{"message":"No query results for model [App\\Models\\Campaign] 17"}},"nonNumeric":{"summary":"Identifiant non numérique","value":{"message":"The route api/v1/campaigns/abc could not be found."}}}}}},"PaymentNotFound":{"description":"Paiement inexistant (`message` = « No query results for model [App\\Models\\Payment] … ») ou appartenant à un autre compte (`message` vide). Un identifiant non numérique (ex. `/payments/abc`) ne correspond à aucune route : 404 avec « The route … could not be found. ».","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unknown":{"summary":"Paiement inexistant","value":{"message":"No query results for model [App\\Models\\Payment] 999"}},"otherAccount":{"summary":"Paiement d'un autre compte","value":{"message":""}},"nonNumeric":{"summary":"Identifiant non numérique","value":{"message":"The route api/v1/payments/abc could not be found."}}}}}},"TooManyRequests":{"description":"Limite de débit dépassée : 600 requêtes par minute et par compte (60 par minute et par adresse IP pour les routes publiques).","headers":{"Retry-After":{"description":"Secondes à attendre avant de réessayer.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Quota de requêtes par minute.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requêtes restantes dans la minute.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpThrottledError"},"examples":{"tooMany":{"summary":"Trop de requêtes","value":{"message":"Trop de requêtes. Réessayez dans 24 secondes.","retry_after":24}}}}}},"IdempotencyConflict":{"description":"Une requête avec la même `Idempotency-Key` est encore en cours de traitement.","headers":{"Retry-After":{"description":"Secondes à attendre avant de réessayer.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"inProgress":{"summary":"Requête en cours","value":{"message":"Une requête avec cette clé d’idempotence est encore en cours de traitement."}}}}}},"ContactNotFound":{"description":"Contact inexistant(e) ou appartenant à un autre compte.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"notFound":{"summary":"Introuvable","value":{"message":"No query results for model [App\\Models\\Contact] 42"}}}}}},"ContactGroupNotFound":{"description":"Groupe de contacts inexistant(e) ou appartenant à un autre compte.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"notFound":{"summary":"Introuvable","value":{"message":"No query results for model [App\\Models\\ContactGroup] 42"}}}}}},"TemplateNotFound":{"description":"Modèle inexistant(e) ou appartenant à un autre compte.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"notFound":{"summary":"Introuvable","value":{"message":"No query results for model [App\\Models\\Template] 42"}}}}}}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"Clé d'idempotence (1 à 255 caractères) : une requête renvoyée avec la même clé dans les 24 heures reçoit la réponse de la première, sans être exécutée ni facturée à nouveau (en-tête `Idempotent-Replayed: true`). Voir la section « Idempotence ».","schema":{"type":"string","maxLength":255},"example":"6f1c2d4e-8a9b-4c3d-9e2f-1a2b3c4d5e6f"}}}}