Un gardien de but peut faire quatre-vingt-dix minutes parfaites. Personne n'en parlera. Il fait une sortie ratée à la 89ᵉ, et c'est le résumé du match.
L'intégration d'un paiement, c'est exactement ce poste. Quand tout se passe bien, personne ne remarque ton travail. Quand une transaction se perd, tu as un client qui a été débité sans rien recevoir, et là, tout le monde regarde.
J'ai intégré Mobile Money sur plusieurs produits — Transfergratis, My Collector, Driivio — et le constat est toujours le même : le chemin nominal représente 10 % du travail.
Le mensonge du tutoriel
Ouvre n'importe quel guide d'intégration MTN MoMo. Tu y trouveras à peu près ceci :
- Obtenir un jeton d'accès
- Appeler
requestToPay - L'utilisateur confirme sur son téléphone
- Interroger le statut
- C'est payé
C'est exact. C'est aussi à peu près aussi utile qu'une recette qui dirait « faire cuire jusqu'à ce que ce soit prêt ».
Ce que le tutoriel ne dit pas :
- L'étape 2 peut répondre
202 Acceptedpuis ne jamais aboutir. La transaction restePENDINGpour toujours. - L'utilisateur peut confirmer après ton timeout. Il est débité, ton système a déjà abandonné.
- L'étape 4 peut répondre
PENDINGpendant quarante secondes puisSUCCESSFUL. OuPENDINGpendant quarante secondes puis plus rien. - Le réseau opérateur peut être en maintenance, et tu ne l'apprends que par les plaintes.
- Le même utilisateur peut lancer trois paiements en tapant trois fois sur ton bouton parce que rien ne se passait visuellement.
La règle numéro un : le client n'initie jamais rien
Je le répète à chaque projet parce que je le vois violé à chaque audit : ton application mobile ne parle jamais directement à l'opérateur.
Les raisons sont évidentes une fois posées :
- Tes identifiants d'API se retrouveraient dans un APK décompilable en dix minutes.
- Un client malveillant peut mentir sur le montant.
- Tu n'as aucune trace serveur de ce qui a été tenté.
Le flux correct :
App → ton backend : « je veux payer la commande 4821 »
ton backend → opérateur : requestToPay(montant vérifié côté serveur)
opérateur → ton backend : webhook de résultat
ton backend → app : notification / mise à jour d'état
Le montant vient de ta base, jamais du client. Ça paraît basique. J'ai audité des applications en production où le montant transitait dans le corps de la requête envoyée par l'app.
Idempotence : le concept qui sauve les nuits
Si tu ne retiens qu'une chose : chaque tentative de paiement doit porter une clé d'idempotence générée avant l'appel.
Concrètement, avant même de contacter l'opérateur, tu écris en base :
paiement_id : uuid généré par toi
commande_id : 4821
montant : 15000
statut : INITIE
tentative : 1
Cet identifiant accompagne l'appel à l'opérateur et sert de référence externe. Ensuite :
- Si l'utilisateur réessaie et que le paiement est déjà
INITIEdepuis moins de N minutes, tu ne relances pas : tu interroges l'existant. - Si le webhook arrive deux fois, tu le traites une seule fois.
- Si ton serveur redémarre en plein vol, tu retrouves les paiements en cours au démarrage.
Sans ça, le scénario classique : l'utilisateur tape trois fois, tu lances trois requestToPay, il confirme trois fois, il est débité trois fois. Tu passeras ton week-end à rembourser à la main.
Webhook et polling : il faut les deux
Les deux mécanismes ont chacun leur défaut, et ils sont complémentaires.
Le webhook est rapide mais peu fiable : ton serveur peut être indisponible à cet instant précis, le réseau peut manger la notification, l'opérateur peut ne pas réessayer.
Le polling est fiable mais coûteux : interroger toutes les deux secondes pendant cinq minutes multiplié par mille transactions, c'est un déni de service que tu t'infliges.
Ce qui fonctionne :
- Webhook comme voie principale. Traité immédiatement, idempotent, réponse
200rapide — tu accuses réception puis tu traites en asynchrone, jamais l'inverse. - Polling en filet, avec un intervalle croissant : 2 s, 5 s, 10 s, 30 s, puis abandon actif au bout de 5 minutes avec passage en
A_VERIFIER. - Un job de réconciliation quotidien qui reprend tous les paiements non résolus et interroge l'opérateur. C'est lui qui rattrape ce que les deux autres ont laissé filer.
Ce job de réconciliation est la pièce que presque personne ne code au départ, et c'est celle qui te sauve. Il transforme un incident client en ligne de log.
Les états, et pourquoi trois ne suffisent pas
La modélisation naïve : EN_ATTENTE, REUSSI, ECHOUE.
Insuffisant. Il te faut au minimum :
| État | Signification | Action |
|---|---|---|
INITIE |
Écrit chez nous, pas encore envoyé | Reprendre au redémarrage |
ENVOYE |
Accepté par l'opérateur | Attendre webhook / poller |
REUSSI |
Confirmé | Livrer le service |
ECHOUE |
Refusé explicitement | Informer, proposer un nouvel essai |
EXPIRE |
Aucune réponse dans le délai | Réconciliation |
A_VERIFIER |
Réconciliation non concluante | Intervention humaine |
Ce dernier état est le plus important, et c'est celui qu'on refuse d'ajouter parce qu'il admet que le système n'est pas parfait. Il l'est. Un paiement sur mille finira là. Sans cet état, ce paiement pollue silencieusement tes statistiques et frustre un client réel.
Ce que tu dois montrer à l'utilisateur
La technique ne suffit pas. Un paiement Mobile Money passe par le téléphone de l'utilisateur — il doit taper son code PIN sur un écran que tu ne contrôles pas.
Ce qui fait la différence :
- Dire ce qui va se passer, avant : « Tu vas recevoir une demande de confirmation sur le 6XX XXX XXX. Saisis ton code PIN. »
- Un compte à rebours visible. L'attente sans repère paraît deux fois plus longue.
- Ne jamais afficher « Échec » pour un timeout. « Nous n'avons pas encore reçu la confirmation. Si tu as été débité, ta commande sera validée automatiquement dans quelques minutes. » C'est vrai, et ça évite un second paiement de panique.
- Un historique consultable. Le premier réflexe d'un utilisateur inquiet est de vérifier. S'il ne peut pas, il appelle. Ou il repaie.
Le problème de fond, pour la profession
Il existe une asymétrie que je trouve significative : la documentation, les bibliothèques, les intégrations toutes faites, les articles de blog — l'immense majorité de l'écosystème est écrite pour Stripe et pour un contexte où la carte bancaire domine.
Or en Afrique centrale et de l'Ouest, le Mobile Money est le moyen de paiement majoritaire. Des dizaines de millions de personnes paient ainsi tous les jours. Et pourtant, le développeur qui doit l'intégrer se retrouve avec une documentation opérateur inégale, pas de SDK digne de ce nom, pas d'environnement de test réaliste, et zéro contenu francophone sérieux.
C'est une des raisons pour lesquelles j'écris ces articles. Le savoir existe, il est juste enfermé dans les têtes de ceux qui l'ont appris à leurs dépens.
Checklist avant la mise en production
- Le backend initie, jamais le client
- Clé d'idempotence générée avant l'appel
- Six états, dont
A_VERIFIER - Webhook idempotent, accusé de réception avant traitement
- Polling à intervalle croissant, avec abandon actif
- Job de réconciliation quotidien
- Journal d'audit de chaque transition d'état
- Messages utilisateur qui distinguent échec et incertitude
- Tests en environnement sandbox, y compris les cas d'échec
- Une procédure écrite pour traiter les
A_VERIFIER
Ce dernier point n'est pas technique, et c'est justement pour ça qu'il manque presque toujours. Quelqu'un doit savoir quoi faire quand un paiement atterrit là, et ce quelqu'un ne doit pas être toi à 23 h.
Pour la mécanique d'intégration elle-même, j'ai écrit un guide complet du flux MTN MoMo et Orange Money. Si tu construis un produit qui doit encaisser en Afrique, parlons-en.
