L'essentiel
  • Les erreurs de webhooks Stripe viennent presque toujours des mêmes causes : une signature mal vérifiée, une mauvaise URL d'endpoint, des events jamais reçus et des doublons créés par les retries.
  • Le correctif tient en quatre réflexes : vérifier la signature avec le bon secret, répondre 200 vite, rendre le traitement idempotent et tout logger.

Lecture complète : 7 min

Les erreurs de webhooks Stripe viennent presque toujours des mêmes causes : une signature mal vérifiée, une mauvaise URL d’endpoint, des events jamais reçus et des doublons créés par les retries. Le correctif tient en quatre réflexes : vérifier la signature avec le bon secret, répondre 200 vite, rendre le traitement idempotent et tout logger.

Un client de Copyboost venait de payer. Stripe affichait le paiement en vert. Et pourtant, son accès ne s’ouvrait pas. Le problème n’était pas le paiement, c’était le webhook, ce message que Stripe envoie à mon app pour lui dire « c’est payé, ouvre l’accès ».

Je build Copyboost, mon outil d’audit de copy, avec l’IA, et je l’ai monté sans coder au départ. Mais quand un webhook casse, personne ne le répare à ma place. J’ouvre les logs de Stripe, je lis l’erreur, je corrige avec l’IA. Voici les quatre erreurs de webhooks qui m’ont le plus coûté, et comment je les ai réglées.

Pourquoi un webhook Stripe casse alors que le paiement passe

Un webhook, c’est l’appel que Stripe envoie à ton serveur après un événement, par exemple un paiement réussi. Si cet appel n’arrive pas, ou si ton app le rejette, le paiement existe chez Stripe mais ton produit ne le sait pas. Le client paie et n’obtient rien. C’est là que tout se joue.

Ce guide suppose Stripe déjà branché et les webhooks configurés ; la mise en place est un autre sujet. Ici, on débogue ce qui casse une fois que tout est censé marcher.

Stripe attend une réponse de ton serveur pour chaque webhook. Un code 2xx veut dire « bien reçu ». Tout le reste, erreur 400, timeout, 500, compte comme un échec. Et un échec, Stripe le réessaie. La plupart des bugs partent de là.

Les erreurs de webhooks Stripe les plus courantes

Quatre erreurs reviennent tout le temps : la signature invalide, la mauvaise URL d’endpoint, les events jamais reçus et les doublons créés par les retries. Chacune a une cause nette et un correctif simple. Le tableau ci-dessous est celui que je garde sous les yeux quand un webhook se met à dérailler.

Tableau des erreurs de webhooks Stripe, leur cause et leur correctif

ErreurCauseCorrectif
Signature invalideLe corps de la requête est modifié avant le calcul, ou le secret ne correspond pasVérifier avec le corps brut et le bon webhook secret
Webhook jamais reçuMauvaise URL d’endpoint, ou event pas coché pour cet endpointCorriger l’URL et activer les events utiles dans le dashboard Stripe
Event traité en doubleStripe réessaie quand il ne reçoit pas de 200 à tempsRendre le traitement idempotent par event id
Accès qui s’ouvre en retardTraitement lourd avant la réponse, Stripe dépasse son délai et retenteRépondre 200 d’abord, faire le travail ensuite
500 dans les logs StripeUne exception non gérée dans le handlerLogger l’erreur, renvoyer le bon code, rejouer l’event

Chaque ligne, je l’ai croisée pour de vrai sur Copyboost. La plus pénible, et la plus fréquente, c’est la première.

Signature invalide : le bug que j’ai corrigé sur Copyboost

La signature invalide, c’est mon premier vrai bug de webhook. Stripe signe chaque message avec un secret. Ton app recalcule la signature et compare. Si le corps de la requête a été modifié avant ce calcul, la comparaison échoue et Stripe reçoit une erreur 400. Le paiement est passé, l’accès reste fermé.

Codecard de vérification de la signature d'un webhook Stripe avec constructEvent

Voici ce que j’ai vécu. Sur Copyboost, mes webhooks renvoyaient tous une erreur 400. Le dashboard Stripe affichait des tentatives en rouge en boucle. Côté app, le log disait « No signatures found matching the expected signature ».

J’ai cherché la cause dans les logs. Le souci venait du corps de la requête. Mon framework le convertissait en JSON avant que je vérifie la signature. Or Stripe signe le corps brut, octet pour octet. En le parsant, je cassais la signature moi-même.

Le correctif tient en une idée : lire le corps brut, sans modification, et le passer tel quel à la vérification. Avec l’IA, j’ai désactivé le parsing automatique sur cette route précise. Les 400 ont disparu, les accès se sont ouverts.

// Le corps doit rester brut, jamais déjà converti en JSON
const signature = request.headers['stripe-signature'];

let event;
try {
  event = stripe.webhooks.constructEvent(
    rawBody,        // corps brut, octet pour octet
    signature,      // en-tête envoyé par Stripe
    webhookSecret   // ton secret whsec_... depuis les variables d'environnement
  );
} catch (err) {
  console.error('Webhook signature invalide', err.message);
  return response.status(400).send('Signature invalide');
}

Le secret, ce whsec_ que Stripe te donne, ne se met jamais en dur dans le code : il vit dans tes variables d’environnement. Une signature qui échoue veut souvent dire un mauvais secret, par exemple celui du mode test laissé en mode live.

Events manqués et doublons : répondre 200 vite et l’idempotence

Deux problèmes liés au même mécanisme : les retries de Stripe. Si ton serveur ne répond pas 200 assez vite, Stripe considère l’event comme perdu et le renvoie. Résultat, soit tu rates l’event, soit tu le traites deux fois. La parade tient en deux gestes : répondre 200 d’abord, et rendre ton traitement idempotent.

Répondre 200 vite, ça veut dire accuser réception avant le gros du travail. Tu reçois l’event, tu vérifies la signature, tu renvoies 200, et seulement après tu ouvres l’accès. Si le traitement lourd passe avant la réponse, Stripe dépasse son délai et renvoie l’event.

Idempotent, c’est le mot qui fait peur pour rien. Ça veut dire qu’exécuter deux fois la même opération donne le même résultat qu’une seule. Je garde l’identifiant de chaque event traité. Avant d’agir, je vérifie si je l’ai déjà vu, et si oui je réponds 200 sans rien refaire.

// Idempotence : on ignore un event déjà traité
const alreadyHandled = await db.wasEventHandled(event.id);
if (alreadyHandled) {
  return response.status(200).send('Event déjà traité');
}

await openAccessForCustomer(event);   // le vrai travail
await db.markEventHandled(event.id);  // on note l'id pour la prochaine fois
return response.status(200).send('OK');

Ce réflexe de lire les logs avant de toucher au code, je l’applique à tous mes bugs serveur. C’est ce qui m’a sauvé sur une erreur EADDRINUSE de port déjà utilisé en Node.js, un cas où le message d’erreur disait déjà tout.

Pour tester sans vrai paiement, la CLI de Stripe rejoue des events vers ton serveur local. Tu déclenches un paiement de test, tu observes ton handler dans les logs, tu corriges avant de passer en live.

Le réflexe qui évite la plupart des bugs de webhooks

Un seul réflexe règle la majorité des cas : traiter chaque webhook dans un ordre fixe. Vérifier la signature, répondre 200 vite, agir de façon idempotente, et tout logger. Si tu tiens ces quatre gestes dans cet ordre, la signature invalide, les events manqués et les doublons disparaissent presque tous.

Liste des réflexes qui évitent la plupart des bugs de webhooks Stripe

Ma check-list, dans l’ordre, pour chaque webhook :

  1. Vérifier la signature avec le corps brut et le bon secret. Pas de signature valide, pas de traitement.
  2. Répondre 200 le plus vite possible. L’accusé de réception d’abord, le travail lourd ensuite.
  3. Rendre l’action idempotente par event id. Un event déjà vu ne déclenche rien deux fois.
  4. Logger chaque étape. Le jour où ça casse, le log te dit où regarder.

Logger, c’est le geste que je néglige le moins. Quand un webhook échoue, je ne devine pas : je relis le dashboard Stripe, qui garde chaque tentative, et mes propres logs. Le message d’erreur pointe presque toujours la bonne ligne.

Ce que débugger un webhook m’a appris

Trois choses à garder. Un webhook qui casse ne touche pas au paiement, il touche à ce que ton client reçoit après, donc c’est une urgence. La signature, les retries et les doublons couvrent la quasi-totalité des pannes, et le même ordre de traitement les règle. Et le log n’est pas une option : c’est lui qui fait d’un bug opaque une simple ligne à corriger.

Je ne devine pas mes bugs, je les lis. C’est ce qui me permet de build en prod sans background dev. Et toi, ton handler de webhook répond-il 200 avant ou après le gros du travail ?

Dernière mise à jour : juin 2026