Pourquoi le même webhook peut s'exécuter deux fois
Les fournisseurs relancent leurs envois, donc votre récepteur peut voir le même événement plus d'une fois. Stripe le dit dans sa documentation et conseille de garder la trace des identifiants d'événement déjà traités (Stripe : événements en double). Quand votre endpoint met trop de temps ou renvoie une erreur, le fournisseur renvoie l'événement, et Stripe continue de relancer un envoi en mode live pendant jusqu'à trois jours, avec un délai exponentiel (Stripe : nouvelles tentatives automatiques). Si rien ne retient que l'événement a déjà été traité, le travail qui en dépend s'exécute à nouveau : un deuxième prélèvement, une deuxième commande, une deuxième ligne dans le CRM. Pour une boutique, un webhook exécuté deux fois peut envoyer deux colis ou deux bons d'achat pour une seule commande.
Une clé stable, réservée une seule fois
La correction, c'est une clé qui ne change pas entre les essais et qui n'est réservée qu'une fois. Chaque nouvel essai de Stripe porte une nouvelle signature et un nouvel horodatage, donc l'en-tête stripe-signature ne peut pas servir de clé. L'identifiant d'événement, si : le modèle utilise evt: suivi de l'identifiant du corps, et quand le corps n'a pas d'identifiant il utilise sha256: suivi du SHA-256 du corps brut. La réservation est une seule instruction sur une clé primaire, INSERT ... ON CONFLICT (key) DO NOTHING RETURNING key, donc un seul envoi récupère la ligne, même quand deux arrivent en même temps. Si une ligne revient, l'étape métier s'exécute. Si aucune ligne ne revient, l'événement a déjà été vu : le récepteur répond quand même 200, et le prélèvement, la commande ou la ligne CRM ne sont pas créés une deuxième fois. L'ordre des contrôles compte aussi pour la sécurité. Vérifiez d'abord la signature du fournisseur (Stripe : vérifier les événements), puis réservez la clé, pour qu'une requête forgée ne puisse pas réserver un vrai identifiant d'événement. Le modèle ne vérifie pas les signatures, ce contrôle se place donc avant lui.
Ce qui s'est passé quand nous l'avons exécuté
Nous avons exécuté le modèle n8n ci-dessous avec Postgres sur notre propre machine, avec n8n 2.42.6 et Postgres 16. Nous avons envoyé l'événement A, puis de nouveau l'événement A avec un autre en-tête de signature, puis l'événement B. n8n a enregistré 3 exécutions et la table métier a reçu 2 lignes. La répétition de A a reçu une réponse 200 et n'a pas exécuté l'étape métier : le nouvel essai a eu sa réponse et rien n'a été écrit deux fois. Nous avons ensuite envoyé deux fois un corps sans identifiant : la clé était le SHA-256 du corps brut, et le deuxième appel a pris la branche doublon. Dans un test séparé, nous avons envoyé à Postgres 8 réservations simultanées d'une même clé avec le SQL que génère cette page : une seule a récupéré la ligne, et l'étape métier s'est exécutée une fois.
La mise en place dans n8n
La personne qui gère n8n peut importer le workflow que nous avons exporté après ce test, sans l'identifiant d'accès du labo, le relier à Postgres et remplacer l'étape métier par ses propres nœuds. Le nœud Webhook utilise le mode de réponse "Using Respond to Webhook node", donc c'est la branche exécutée qui répond. Le nœud Code construit la clé, un nœud Postgres fait la réservation, et "Row Returned?" envoie un nouvel événement vers l'étape métier et une répétition vers "Respond 200 duplicate".
Modèle de workflow n8n (Webhook, Code, Postgres, IF, Respond to Webhook)
Le workflow exporté en fichier .json, prêt à importer.
-
Créez les tables
CREATE TABLE IF NOT EXISTS processed_events ( key text PRIMARY KEY, first_seen timestamptz NOT NULL DEFAULT now() ); -- Table métier d'exemple utilisée par le modèle. Remplacez-la par votre propre étape. CREATE TABLE IF NOT EXISTS business_log ( id bigserial PRIMARY KEY, event_key text NOT NULL, event_type text, created_at timestamptz NOT NULL DEFAULT now() );
-
Réglez l'environnement et les identifiants
n8n auto-hébergé bloque par défaut les modules intégrés dans le nœud Code. Définissez
NODE_FUNCTION_ALLOW_BUILTIN=cryptosur le conteneur n8n pour querequire('crypto')fonctionne. Après l'import, ouvrez les deux nœuds Postgres et choisissez votre identifiant Postgres (hôte, port, base de données, utilisateur, mot de passe). n8n peut aussi enregistrer les données d'exécution, y compris le corps du webhook, selon vos réglages d'exécution. Si le corps contient des données personnelles, vérifiez ces réglages et leur durée de conservation. -
Importez, branchez votre étape, publiez
Dans n8n, importez le fichier .json. Remplacez "Business Step (insert business_log)" par vos propres nœuds, gardez-les sur la branche true de "Row Returned?", puis publiez le workflow. Placez votre contrôle de signature avant le nœud Code si votre fournisseur signe ses requêtes.
-
Testez avec un envoi répété
Envoyez deux fois le même événement et une fois un autre événement. Dans Executions, la première et la troisième exécution doivent passer par l'étape métier, et la deuxième prendre "Respond 200 duplicate".
-
Nettoyez les anciennes clés
Le nettoyage est une étape séparée, hors du workflow du webhook. Gardez les clés plus longtemps que la durée pendant laquelle le fournisseur peut renvoyer. Stripe permet un renvoi manuel depuis le Dashboard pendant jusqu'à 15 jours et depuis la CLI pendant jusqu'à 30 jours (Stripe : nouvelles tentatives manuelles). Un workflow n8n planifié ou une tâche cron peut exécuter :
DELETE FROM processed_events WHERE first_seen < now() - interval '30 days';
Limite : la clé est réservée avant que l'étape métier s'exécute. Si l'étape métier échoue après la réservation, un nouvel essai de cet événement est traité comme un doublon. Ajoutez un chemin d'erreur qui exécute DELETE FROM processed_events WHERE key = $1 pour cette clé, ou corrigez et rejouez l'événement à la main.
Ce qui est stocké, et ce qui reste dans votre navigateur
La clé de cette page est calculée avec crypto.subtle.digest dans votre navigateur, et le script de la page ne fait aucune requête réseau avec ce que vous collez. Dans Postgres, processed_events stocke la clé et un horodatage. La clé est l'identifiant d'événement du fournisseur, ou un SHA-256 du corps quand il n'y a pas d'identifiant. La table d'exemple business_log stocke la clé et le type d'événement, et votre propre étape métier décide de ce qui est stocké en plus. Le planificateur ci-dessus génère la même réservation que le modèle n8n : INSERT ... ON CONFLICT (key) DO NOTHING RETURNING key, une branche selon la ligne renvoyée et un DELETE de nettoyage séparé. Ces notes décrivent le code de cette page et du modèle. Ce n'est pas une certification.