Aller au contenu principal

Idempotence

Sur un réseau, une requête peut arriver deux fois, ou sans que sa réponse vous parvienne. L'idempotence garantit qu'un même paiement n'est ni débité ni enregistré deux fois.

Webhooks reçus en double​

Un même événement peut vous être livré plusieurs fois : une relance, si votre réponse s'est perdue. Enregistrez l'en-tête X-Webhook-ID de chaque événement traité, et ignorez ceux déjà vus, en répondant tout de même 200.

Quelques règles complètent ce contrôle :

  • Ne vous fiez pas à l'ordre d'arrivée : fondez-vous sur status. Une commande payée ne revient jamais en arrière.
  • Ne livrez que sur SUCCEEDED, après avoir contrôlé amount, currency et votre référence (reference ou link_metadata).
  • Ignorez "test": true en production : aucun argent n'a été encaissé.
traitement.py
from decimal import Decimal

from django.db import IntegrityError, transaction


def traiter(event: dict, event_id: str):
"""Appelée après la vérification de la signature. Sans effet si l'événement est déjà traité."""
if event.get("test") and not settings.DEBUG:
return

try:
with transaction.atomic():
# Contrainte d'unicité sur event_id : deux livraisons simultanées ne passent pas toutes les deux
WebhookRecu.objects.create(event_id=event_id)

order_id = (event.get("link_metadata") or {}).get("order_id") or event.get("reference")
order = Order.objects.select_for_update().get(id=order_id)

if (
event["status"] == "SUCCEEDED"
and order.status != "paid"
and Decimal(event["amount"]) == order.total
and event["currency"] == "XOF"
):
order.status = "paid"
order.payment_id = event["payment_id"]
order.save(update_fields=["status", "payment_id"])
except IntegrityError:
pass # événement déjà traité

Si votre traitement est long (e-mails, factures), confiez-le à une file de tâches et répondez 200 tout de suite : au-delà de 10 secondes, l'envoi est considéré comme échoué et sera relancé.

Idempotency-Key des paiements directs​

Pour le paiement direct, envoyez un en-tête Idempotency-Key unique par tentative de paiement (100 caractères au maximum, valable 24 heures).

SituationRéponse
Même clé, même contenu, paiement déjà crééLe paiement existant, avec l'en-tête Idempotent-Replayed: true, sans nouvel appel à l'opérateur
Même clé, requête encore en cours de traitement409 idempotency_conflict : réessayez quelques secondes plus tard
Même clé, autre contenu (montant, numéro, moyen de paiement ou référence)422 idempotency_key_reused : utilisez une nouvelle clé
Opérateur injoignable (502)Aucun débit : renvoyez avec la même clé
500 payment_unrecordedLe client a peut-être été débité : ne renvoyez pas, contactez le support

Une bonne clé combine votre identifiant de commande et un numéro de tentative : commande-1234-essai-1. Enregistrez-la avant l'appel, et réutilisez-la telle quelle pour tout renvoi de la même tentative.