Idempotence & rejeu
Rejouer un appel NguePay sans payer, rembourser ou virer deux fois.
Sur un réseau, une requête sans réponse ne veut rien dire. Le serveur peut ne l'avoir jamais reçue, l'avoir traitée puis perdu la réponse, ou être en train de la traiter. Votre client HTTP, lui, va réessayer — c'est son travail.
Sur une gateway de paiement, ce réessai doit être sans conséquence. Voici ce que NguePay garantit, et ce que vous devez fournir pour en bénéficier.
En un coup d'œil
| Opération | Clé | Fournie par | Au rejeu |
|---|---|---|---|
POST /v1/payments | reference | vous | rend la transaction existante (200) |
POST /v1/checkout/sessions | reference | vous | rend la session existante |
POST /v1/payouts | reference | vous | rend le virement existant (200) |
POST /v1/refunds | Idempotency-Key | vous, facultatif | rend le remboursement existant (200) |
| Webhooks entrants opérateur | signature | l'opérateur | ignoré |
Le code HTTP distingue les deux cas : 201 à la création, 200 au rejeu. Le corps est identique — si vous ne regardez que le corps, vous n'avez rien à changer.
Paiements et virements : votre référence suffit
La reference que vous envoyez est la clé. Une même référence chez un même marchand ne peut produire qu'un seul paiement — et, côté virements, qu'un seul virement.
curl -X POST https://api.live.nguepay.com/api/v1/payments \
-H "Authorization: Bearer sk_live_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"provider": "orange",
"reference": "cmd_2026_00412",
"phone": "+2250700000000"
}'Rejouez exactement le même appel : vous recevez la même transaction, avec le même transaction_id, et un code 200 au lieu de 201.
La garantie n'est pas une vérification, c'est une contrainte
C'est important si vous appelez depuis plusieurs processus.
NguePay commence par chercher si la référence existe. Mais entre cette lecture et l'écriture il y a une fenêtre, et deux requêtes simultanées s'y engouffrent : toutes deux lisent « rien », toutes deux créent. La vérification préalable n'est donc qu'un confort.
Ce qui garantit réellement l'unicité, c'est une contrainte d'unicité en base. Quand deux requêtes se croisent, la base tranche, et NguePay rattrape le refus pour rendre au perdant la transaction du gagnant.
En pratique : dix appels vraiment simultanés sur cmd_2026_00412 créent une transaction et renvoient dix fois le même transaction_id. Aucun ne reçoit d'erreur.
Une seule requête initie le paiement chez l'opérateur — celle qui a gagné la course. Les autres reçoivent la transaction telle qu'elle est à cet instant : son checkout_url peut donc être encore null si vous êtes arrivé pendant l'appel à l'opérateur. Relisez-la avec GET /v1/payments/{uuid} si vous avez besoin de l'URL tout de suite.
Une même référence, un autre montant : refus
Rejouer suppose redemander la même chose. Si la référence existe déjà pour un montant différent, ce n'est pas un rejeu — c'est une collision, et NguePay répond 422 NGP005 au lieu de rendre la transaction existante. La règle vaut pour POST /v1/payments, POST /v1/checkout/sessions et POST /v1/payouts.
{
"success": false,
"error_code": "NGP005",
"message": "La référence « cmd_2026_00412 » est déjà utilisée pour un montant différent."
}Ce refus protège d'une confusion coûteuse. Votre pk_ est lisible dans le source de votre boutique : n'importe qui peut s'en servir pour créer une session sur une référence de son choix. Sans ce contrôle, une session de 100 XOF posée sur cmd_2026_00412 faisait répondre « 200 » à votre demande serveur de 50 000 XOF — et vous rendait la transaction de l'attaquant.
Que faire :
- si vous rejouez un appel, vérifiez que le montant est le même — c'est presque toujours l'explication ;
- si le montant a légitimement changé, utilisez une nouvelle référence : l'ancienne commande n'est plus la même ;
- si ni l'un ni l'autre, quelqu'un a pris la référence avant vous. Vérifiez la transaction dans le dashboard, et signalez-le au support.
Choisir une bonne référence
- Stable — la même commande doit toujours donner la même référence. Si vous la régénérez au réessai, l'idempotence ne peut rien pour vous.
- Difficile à deviner — une référence séquentielle (
CMD-1,CMD-2…) se prédit, donc se prend de vitesse. Un suffixe aléatoire suffit :CMD-1042-7f3a9c. - À vous — elle n'a besoin d'être unique que chez vous. Deux marchands peuvent utiliser
CMD-1001sans se gêner. - Sans donnée personnelle — elle apparaît dans le dashboard, les exports et les webhooks.
Remboursements : fournissez Idempotency-Key
Un remboursement n'a pas de référence naturelle. Deux remboursements partiels de 500 XOF sur la même transaction sont une demande parfaitement légitime, et NguePay n'a aucun moyen de deviner que le second est un réessai du premier. C'est donc à vous de le dire.
curl -X POST https://api.live.nguepay.com/api/v1/refunds \
-H "Authorization: Bearer sk_live_VOTRE_CLE" \
-H "Idempotency-Key: remb_cmd_2026_00412" \
-H "Content-Type: application/json" \
-d '{"transaction_uuid": "9f1c0c2e-...", "amount": 500}'Rejouer la même clé sur la même transaction rend le remboursement déjà enregistré, sans rien renvoyer à l'opérateur : une clé donne lieu à un seul appel opérateur, même si deux requêtes arrivent en même temps.
Comme pour les références, la même clé avec un autre montant n'est pas un rejeu : elle est refusée en 422 (NGP002, « Clé d'idempotence déjà utilisée pour un remboursement d'un autre montant. »).
Si vos en-têtes ne sont pas à votre main (formulaire, plateforme no-code), le champ de corps idempotency_key fait la même chose. L'en-tête l'emporte quand les deux sont présents.
Sans clé, deux appels identiques produisent deux remboursements. Ce n'est pas un défaut : refuser le second reviendrait à interdire un remboursement partiel légitime. Fournissez une clé dès que votre client HTTP peut rejouer tout seul — c'est-à-dire presque toujours.
Le cumul ne dépasse jamais le montant
Quoi qu'il arrive, la somme des remboursements reste inférieure ou égale au montant de la transaction. Un dépassement répond 422 NGP002.
Les remboursements décidés hors NguePay — depuis la console Stripe, par exemple — comptent au même titre dès que le webhook nous les apprend. Ce point compte plus qu'il n'en a l'air : s'ils n'étaient pas décomptés, vous pouviez ensuite rembourser la totalité par l'API, et rendre l'argent deux fois.
Webhooks entrants : rien à faire
NguePay écarte les rejeux côté opérateur. Une notification captée puis renvoyée est ignorée, même si sa signature est valide et son horodatage dans la fenêtre — ce qui est précisément le cas d'un rejeu.
Le statut d'une transaction ne remonte jamais : une fois succeeded, failed, expired ou canceled, elle ne bouge plus.
Webhooks sortants : à vous de jouer
NguePay peut livrer deux fois le même événement — c'est le prix d'une livraison garantie. Rendez votre traitement idempotent, en vous appuyant sur l'en-tête X-NguePay-Delivery ou sur le couple event + transaction_id.
const cle = `${payload.event}:${payload.data.transaction_id}`;
if (await dejaTraite(cle)) return res.status(200).end();
await traiter(payload);
await marquerTraite(cle);
res.status(200).end();Répondez 200 même sur un doublon : un 4xx ou un 5xx déclenche une nouvelle tentative, et vous verrez le même événement revenir. Détails sur la page Webhooks.
Ce qui n'est pas idempotent
POST /v1/webhook-endpoints/{uuid}/test— envoie une livraison de test à chaque appel, c'est son objet.POST /v1/keys— chaque appel crée une paire de clés. Révoquez celles dont vous n'avez pas besoin.POST /v1/payout-accounts/mobile— chaque appel déclenche un SMS OTP (6 par minute maximum).POST /v1/payout-accounts/{uuid}/resend-otp— renvoie un nouveau code : un par minute pour un compte, 10 par heure pour le marchand.