Construire · Avancé
Webhooks de paiement manqués : le paiement a réussi, la boutique n’en a rien su
Un client paie, le prestataire confirme, et la notification destinée à votre boutique se perd, arrive en retard ou arrive deux fois. Voici comment concevoir le traitement des commandes pour que les webhooks de paiement manqués ne coûtent rien : pas de commande payée qui semble impayée, pas de commande en double, pas de second débit.

Photo : domaine public (CC0)
Dans ce guide
Où une notification de paiement peut mal tourner
Un paiement par carte ou par portefeuille électronique fait intervenir trois parties qui ne partagent pas de base de données : le navigateur du client, le prestataire de paiement et votre boutique. Le prestataire connaît le résultat en premier. Votre boutique l’apprend de deux façons : le navigateur du client est redirigé vers une page de retour, et le prestataire envoie une notification de serveur à serveur, appelée le plus souvent webhook.
Aucun de ces deux canaux n’est fiable à lui seul. Le client peut fermer l’onglet avant la redirection. Le webhook peut atteindre votre serveur pendant un déploiement, un dépassement de délai ou une panne de base de données. Comme les prestataires renvoient les notifications dont ils n’ont pas reçu l’accusé de réception, le même événement peut aussi arriver deux fois, ou arriver des heures plus tard, après un événement plus récent concernant le même paiement.
La conséquence pratique : la livraison des webhooks se fait « au moins une fois, sans ordre garanti ». Une boutique qui suppose « exactement une fois, dans l’ordre » finira tôt ou tard par afficher une commande payée comme impayée ou par expédier deux fois. Si vous préparez une intégration de paiement, concevez-la ainsi dès le premier jour.
| Cas de défaillance | Symptôme dans la boutique | Garde-fou |
|---|---|---|
| La notification n’arrive jamais | La commande reste « en attente de paiement » alors que l’argent a été prélevé | Tâche de rapprochement qui interroge le prestataire |
| La notification arrive deux fois | Deux e-mails de confirmation, stock diminué deux fois | Identifiant d’événement unique et changement de statut conditionnel |
| La notification arrive en retard | La commande a été annulée comme impayée, puis le paiement apparaît | Vérifier l’état chez le prestataire avant d’annuler, traiter le cas « payé après annulation » |
| Les événements arrivent dans le désordre | Un remboursement est traité avant que le paiement soit connu | Machine à états qui refuse les transitions impossibles et redemande l’état |
| Le gestionnaire plante après l’accusé de réception | L’événement a disparu, rien n’a été traité | Stocker durablement l’événement avant de répondre |
| Notification falsifiée | Commande marquée payée sans aucun paiement | Vérification de la signature sur le corps brut de la requête |
| La boutique relance sa propre requête après un dépassement de délai | Le client est débité ou remboursé deux fois | Clé d’idempotence sur chaque requête sortante |
Traitement durable : stocker d’abord, accuser réception vite, travailler depuis une file d’attente
Le point d’entrée qui reçoit les webhooks devrait faire très peu de choses. Tout son travail tient en quatre étapes :
- Lire le corps brut de la requête et vérifier la signature.
- Insérer l’événement dans une table d’événements, avec l’identifiant d’événement du prestataire sous une contrainte d’unicité.
- Répondre par un statut 2xx dès que l’insertion est validée en base.
- Laisser tout le reste à un worker qui lit les événements non traités.
Si vous accusez réception avant que l’événement soit stocké et que le processus s’arrête, le prestataire croit que vous l’avez et ne le renvoie plus jamais. Si vous faites le vrai travail (marquer la commande comme payée, émettre la facture, appeler l’entrepôt) avant d’accuser réception, une dépendance lente fait expirer la requête, le prestataire la renvoie, et vous traitez le même paiement en parallèle.
Si la base de données est indisponible, répondez par un statut 5xx pour que le prestataire réessaie plus tard. Ne répondez jamais 2xx pour un événement que vous n’avez pas pu stocker.
Le worker prend les événements dans la table, ou dans une file d’attente que celle-ci alimente. Les files d’attente livrent elles aussi au moins une fois : le worker doit donc pouvoir traiter deux fois le même événement sans danger. Après un nombre fixé de tentatives échouées, placez l’événement dans un état de « lettre morte » et alertez une personne.
Idempotence : pouvoir répéter « marquer la commande comme payée » sans danger
Idempotent signifie qu’exécuter une opération deux fois a le même effet que l’exécuter une fois. Trois techniques réunies y parviennent.
Dédoublonner par identifiant d’événement
La contrainte d’unicité sur l’identifiant d’événement du prestataire empêche de stocker deux fois la même livraison. Elle n’aide pas quand deux événements différents décrivent le même paiement.
Changer d’état sous condition
N’écrivez pas « passer le statut à payé ». Écrivez la transition : passer le statut à payé là où l’identifiant de commande correspond et où le statut est encore « en attente de paiement ». La base de données indique combien de lignes ont changé. Les effets de bord ne s’exécutent que si exactement une ligne a changé : diminuer le stock, créer la facture, mettre en file l’e-mail de confirmation. Une seconde exécution change zéro ligne et ne fait rien.
Enregistrez les effets de bord dans la même transaction que le changement de statut, sous forme de lignes dans une table d’envoi (outbox) qu’un expéditeur traite l’une après l’autre. Sinon, un plantage entre « payé » et « envoyer l’e-mail » laisse une commande payée et jamais confirmée.
Utiliser des clés d’idempotence sur les requêtes sortantes
Le même problème existe dans l’autre sens. Quand votre boutique demande au prestataire d’encaisser ou de rembourser un paiement et que la réponse n’arrive pas dans le délai, vous ne savez pas si l’opération a eu lieu. La plupart des API de paiement acceptent une clé d’idempotence : une valeur unique que vous générez une fois par opération voulue et que vous renvoyez inchangée à chaque nouvelle tentative, pour que le prestataire ne l’exécute qu’une fois. Stockez la clé avec la commande avant la première tentative, pas en mémoire.
Avant de vous fier à une notification, comparez aussi son montant et sa devise avec ceux de la commande. Un événement valide pour le mauvais montant n’est pas une commande payée.
Rapprochement : demander au lieu d’attendre
Les prestataires cessent de réessayer au bout d’un certain temps, et un point d’entrée mal configuré pendant une journée aura tout manqué. Le rapprochement est le filet de sécurité : une tâche planifiée qui demande la vérité au prestataire.
- Commandes ouvertes : pour chaque commande restée « en attente de paiement » depuis plus de quelques minutes, interrogez le prestataire à l’aide de la référence de paiement stockée et faites passer la réponse par le même gestionnaire que celui du webhook.
- Sens inverse : une fois par jour, listez les paiements réussis du prestataire pour la période et vérifiez que chacun correspond à une commande à l’état payé, pour le même montant.
- Avant d’annuler : une commande n’est annulée comme impayée qu’après que le prestataire a confirmé qu’aucun paiement n’a réussi ni n’est en cours.
Si le rapprochement et le worker des webhooks appellent tous deux la même transition conditionnelle, peu importe lequel arrive le premier.
Les chiffres ci-dessous sont un exemple illustratif, pas des mesures : une boutique avec 10 000 paiements réussis en un mois, dont 150 (1,5 %) ne sont pas arrivés sous la forme d’une seule notification propre à la première tentative. 96 + 42 + 12 = 150.
Exemple illustratif : 150 paiements sur 10 000 avec une notification imparfaite
Avec le dédoublonnage et le rapprochement, les 150 aboutissent au même état que les 9 850 autres.
Une conception de reprise sans commande ni débit en double
Les commandes en double ont le plus souvent une seule cause : la commande est créée à deux endroits, une fois sur la page de retour et une fois par le webhook. Le remède consiste à la créer une seule fois, avant le paiement.
Un paiement qui arrive pour une commande déjà annulée demande une règle de gestion, pas seulement du code : rouvrir la commande si le stock est encore là, sinon rembourser automatiquement et prévenir le client. Décidez-le à l’avance. Pour une boutique en ligne existante, l’audit le plus rapide consiste à rejouer deux fois un webhook stocké dans un environnement de test et à regarder ce qui se passe. Les mêmes principes valent pour toute intégration d’API qui reçoit des événements d’un autre système.
Les questions qu’on nous pose souvent
Vous préférez que ce soit réglé sans vous en charger ?
Dites-nous sur quoi vous travaillez. Une personne vous répond sous un jour ouvré.
