3 214,80 € réglés deux fois pour la même facture fournisseur : c’est le montant exact qui a déclenché la vérification comptable ayant révélé l’incident décrit ici. L’agent chargé de régler les factures fournisseurs arrivées à échéance avait bel et bien exécuté deux paiements distincts pour une seule et même facture, à quelques minutes d’intervalle.
Ce texte revient sur le diagnostic de cet incident et sur la vérification d’idempotence qui manquait avant que l’agent ne déclenche une action financière. Il ne traite pas du choix du prestataire de paiement utilisé, resté le même avant et après correctif.
Le déroulé exact de l’incident
L’agent parcourait la liste des factures fournisseurs marquées comme échues et appelait, pour chacune, un outil payer_facture connecté à l’API du prestataire de paiement. Pour la facture en cause, l’appel initial a bien été transmis, mais la réponse n’est jamais arrivée jusqu’à l’agent : le service de paiement a traité la demande normalement, tandis qu’une coupure réseau de quelques secondes a empêché l’agent de recevoir la confirmation.
Pourquoi l’agent a rejoué l’appel
N’ayant reçu aucune confirmation, l’agent a interprété l’absence de réponse comme un échec de la tentative initiale et a relancé l’appel de l’outil pour la même facture, conformément à sa logique de nouvelle tentative en cas d’échec réseau. Rien, dans les informations dont il disposait, ne lui permettait de distinguer un échec réel d’un simple délai de confirmation sur une action déjà exécutée avec succès.

L’absence d’identifiant reliant facture et tentative de paiement
La cause profonde ne se situait pas dans la logique de nouvelle tentative de l’agent, raisonnable en soi, mais dans l’absence côté outil d’un mécanisme capable de reconnaître qu’une demande de paiement pour cette facture précise avait déjà été traitée. L’outil acceptait chaque appel comme une nouvelle demande indépendante, sans vérifier si un paiement correspondant à cette facture existait déjà.
function agence_payer_facture_callback( $params ) {
$facture_id = $params['facture_id'];
// Absent avant le correctif : aucune vérification d'un paiement déjà existant
$paiement = $prestataire->creer_paiement( array(
'montant' => agence_montant_facture( $facture_id ),
'reference' => 'facture-' . $facture_id,
) );
return $paiement;
}
Le correctif : une clé d’idempotence portée par la facture elle-même
Le correctif a consisté à transmettre, à chaque appel, une clé d’idempotence dérivée de l’identifiant de la facture plutôt que générée aléatoirement à chaque tentative. Le prestataire de paiement utilisé reconnaît nativement ce mécanisme : une même clé soumise deux fois renvoie la réponse du premier traitement sans déclencher un second paiement.
function agence_payer_facture_callback( $params ) {
$facture_id = $params['facture_id'];
$cle_idempotence = 'facture-' . $facture_id . '-v1';
$paiement = $prestataire->creer_paiement( array(
'montant' => agence_montant_facture( $facture_id ),
'reference' => 'facture-' . $facture_id,
'cle_idempotence' => $cle_idempotence,
) );
return $paiement;
}
Ce que ce correctif ne couvre pas à lui seul
La clé d’idempotence protège contre un rejeu du même appel avec les mêmes paramètres. Elle ne protège pas contre une erreur de logique qui déclencherait, pour une même facture, deux appels avec des clés différentes générées séparément. Sur ce projet, un second garde-fou a donc été ajouté indépendamment : une vérification, avant tout appel de paiement, de l’absence d’un paiement déjà enregistré pour cette facture dans la base locale, redondante avec la clé d’idempotence mais couvrant un scénario distinct.
- Clé d’idempotence dérivée d’un identifiant stable, jamais générée aléatoirement à chaque tentative.
- Vérification locale de l’absence de paiement existant, en complément, pas en remplacement.
- Journalisation systématique de chaque appel de paiement, y compris ceux bloqués par ces vérifications.
Une action financière déclenchée par un agent doit toujours pouvoir être rejouée sans risque : si le rejeu peut produire un second effet, ce n’est pas l’agent qu’il faut corriger en premier, mais l’outil qu’il appelle.
En résumé
Un agent qui rejoue un appel après un délai réseau applique une logique de robustesse tout à fait raisonnable ; le risque ne vient pas de cette logique, mais de l’absence, côté outil, d’un mécanisme capable de reconnaître qu’une action irréversible a déjà été exécutée. Toute action financière confiée à un agent devrait porter une clé d’idempotence dès sa première mise en production, pas seulement après un premier incident.