Aller au contenu

Publié le 12/10/2026

Construire un récepteur de webhooks idempotent avec Python et PostgreSQL

Dans l'architecture moderne des applications web, les webhooks sont devenus incontournables. Qu'il s'agisse de confirmer un paiement Stripe, d'enregistrer une action GitHub ou de synchroniser un CRM, ils permettent une communication asynchrone efficace entre services tiers et votre backend.

Cependant, un principe fondamental régit les systèmes d'envoi de webhooks : la garantie de livraison au moins une fois (at-least-once delivery). Si l'émetteur ne reçoit pas votre confirmation HTTP 200 à temps en raison d'une micro-coupure réseau, il réessaiera. Votre serveur peut ainsi recevoir plusieurs fois le même événement. Sans précaution, vous risquez de créditer deux fois un compte client ou de déclencher des e-mails en double.

La solution ? Rendre votre récepteur de webhooks idempotent. Une opération idempotente produit le même résultat, qu'elle soit exécutée une seule fois ou répétée dix fois avec les mêmes paramètres.

La stratégie : PostgreSQL comme garant de la vérité

La plupart des plateformes fournissent un identifiant unique pour chaque événement (comme id dans Stripe ou x-delivery dans GitHub). Nous pouvons nous appuyer sur ce point d'ancrage en base de données pour filtrer les doublons au niveau transactionnel.

Plutôt que de vérifier l'existence de l'événement puis de l'insérer (ce qui introduit une faille de concurrence ou race condition), nous déléguons cette vérification à PostgreSQL grâce à une contrainte d'unicité et à l'instruction atomique ON CONFLICT DO NOTHING.

Une table simple suffit pour structurer ce mécanisme :

  • event_id (VARCHAR, clé primaire ou index unique) : l'identifiant fourni par l'émetteur.
  • provider (VARCHAR) : la source du webhook (ex. stripe, shopify).
  • status (VARCHAR) : l'état du traitement (PENDING, PROCESSED, FAILED).
  • payload (JSONB) : les données brutes reçues.
  • created_at (TIMESTAMP) : la date de réception.

L'implémentation côté Python

Avec un framework moderne comme FastAPI ou Flask, le flux de travail doit être rapide et sécurisé. Voici les étapes à orchestrer :

  • Valider la signature : Avant toute interaction avec la base, vérifiez la signature cryptographique du payload pour prouver l'authenticité de l'expéditeur.
  • Tenter l'insertion : Exécutez une requête INSERT INTO webhook_events ... ON CONFLICT (event_id) DO NOTHING RETURNING id;.
  • Évaluer le résultat : Si la requête ne renvoie aucune ligne, l'événement a déjà été reçu. Renvoyez immédiatement un code HTTP 200 pour signaler à l'émetteur que tout est en ordre, sans exécuter à nouveau votre logique métier.
  • Traiter l'événement : Si la ligne a été insérée, procédez à vos opérations métiers, puis mettez à jour le statut en base à PROCESSED.

Aller plus loin : découplage et asynchronisme

Pour garantir des temps de réponse sous la barre des 500 millisecondes exigée par la plupart des fournisseurs, évitez les traitements lourds directement dans le cycle requête-réponse HTTP. La meilleure pratique consiste à persister l'événement avec un statut PENDING, renvoyer un statut HTTP 200 au webhook, et déléguer le travail de fond à une file de tâches (comme Celery, RQ ou les tâches d'arrière-plan de FastAPI).

En combinant la rigueur transactionnelle d'une base ACID comme PostgreSQL avec la flexibilité de Python, vous transformez un point de défaillance classique en une brique d'infrastructure résiliente et prête pour la production.