Webhooks en production : fiabiliser les intégrations qui déclenchent vos workflows

par Benjamin

Un webhook semble simple : un service envoie une requête HTTP, votre application la reçoit et déclenche une action.

Cette simplicité disparaît dès que le webhook devient important pour votre activité. Un paiement confirmé doit créer une commande. Un abonnement modifié doit ajuster les droits. Un ticket créé doit lancer une automatisation. Si la requête est perdue, reçue deux fois, traitée dans le mauvais ordre ou forgée par un tiers, votre système peut produire un état incohérent.

Un webhook n'est donc pas un appel HTTP ordinaire. C'est un message externe, envoyé sur un réseau que vous ne contrôlez pas, avec une livraison généralement au moins une fois et un contrat qui peut évoluer.

Voici les principes que je recommande pour construire une intégration webhook réellement exploitable en production.

Un webhook ne garantit pas une livraison unique

Le premier piège consiste à traiter chaque requête reçue comme si elle était nouvelle.

Les fournisseurs réessaient généralement une livraison lorsque votre endpoint ne répond pas correctement ou met trop de temps à répondre. Stripe indique aussi qu'un même événement peut être livré plusieurs fois et que l'ordre de livraison n'est pas garanti. [^1]

Votre endpoint doit donc être conçu pour accepter :

  • un événement reçu plusieurs fois ;
  • un événement reçu après un autre événement plus récent ;
  • une livraison interrompue après l'exécution métier mais avant la réponse HTTP ;
  • un événement ancien qui est rejoué manuellement ;
  • un événement valide mais impossible à traiter immédiatement.

La bonne question n'est pas "comment éviter que le fournisseur réessaie ?". C'est "que se passe-t-il si cette même notification arrive encore demain ?".

Vérifier l'authenticité avant toute action

Un endpoint webhook est public par nature. Toute personne qui connaît son URL peut tenter de lui envoyer une requête.

Ne faites jamais confiance à un champ comme event_type, customer_id ou status pour décider si une action métier est légitime. Ces valeurs doivent être acceptées uniquement après vérification de l'authenticité du message.

La plupart des fournisseurs sérieux signent le contenu envoyé avec un secret partagé ou une clé dédiée. Stripe recommande de vérifier la signature de chaque événement avec le corps brut de la requête, avant de traiter son contenu. GitHub utilise également une signature HMAC dans l'en-tête X-Hub-Signature-256. [^1][^2]

Une vérification correcte implique généralement :

  • récupérer le corps brut, sans le re-sérialiser avant le calcul de la signature ;
  • lire l'en-tête de signature attendu ;
  • calculer la signature avec le secret stocké côté serveur ;
  • comparer les valeurs avec une fonction résistante aux attaques temporelles ;
  • vérifier la fraîcheur du message lorsque le fournisseur inclut un horodatage ;
  • refuser la requête avant toute lecture métier si la signature est absente ou invalide.

Le secret de signature doit rester dans un gestionnaire de secrets ou une variable d'environnement. Il ne doit apparaître ni dans le dépôt, ni dans les logs, ni dans une URL.

La signature prouve que le message a été produit par la source qui possède le secret. Elle ne prouve pas que le contenu est cohérent avec votre métier. Les deux contrôles sont nécessaires.

Rendre le traitement idempotent

L'idempotence signifie qu'appliquer deux fois le même événement produit le même résultat métier qu'une seule application.

Supposons que votre application reçoive un événement payment.succeeded. Elle crée la commande, puis tombe en panne avant de répondre 200. Le fournisseur renvoie l'événement. Sans protection, vous pouvez créer deux commandes ou envoyer deux confirmations.

La protection habituelle consiste à enregistrer l'identifiant unique de l'événement dans une table durable, avec son état de traitement. Avant de déclencher l'action, votre application vérifie si cet identifiant a déjà été traité.

Une implémentation robuste doit aussi gérer la concurrence. Deux tentatives identiques peuvent arriver presque simultanément. La vérification et l'enregistrement doivent donc être protégés par une contrainte d'unicité en base ou par une transaction adaptée. Un simple if suivi d'un insert sans contrainte laisse passer les courses concurrentes.

Conservez au minimum :

  • l'identifiant de l'événement ;
  • le fournisseur et le type d'événement ;
  • la date de première réception ;
  • le statut de traitement ;
  • la date de fin ou d'échec ;
  • un identifiant de corrélation vers l'opération métier.

Ne déduisez pas l'idempotence du contenu JSON. Deux événements différents peuvent décrire un état proche, et un même événement peut contenir des champs qui évoluent lors d'un rejeu. Utilisez l'identifiant contractuel fourni par l'émetteur, complété par une clé métier lorsque votre cas d'usage l'exige.

Séparer réception et traitement métier

Un endpoint webhook ne devrait pas effectuer tout le travail avant de répondre.

La réception doit rester courte : vérifier la signature, valider la structure minimale, enregistrer le message et le placer dans une file ou un mécanisme de traitement fiable. Le worker peut ensuite appeler votre logique métier, les API internes et les services externes.

Cette séparation apporte plusieurs bénéfices :

  • le fournisseur reçoit rapidement un accusé de réception ;
  • les traitements longs ne provoquent pas de timeout inutile ;
  • les échecs temporaires peuvent être retentés sans perdre le message ;
  • la charge peut être absorbée par une file ;
  • les événements peuvent être inspectés et rejoués ;
  • l'endpoint public reste plus simple à protéger.

Répondre rapidement ne signifie pas répondre 200 avant d'avoir conservé le message. Si vous accusez réception puis perdez le payload, le fournisseur considère la livraison réussie alors que votre système ne peut plus la traiter.

Le flux recommandé ressemble à ceci :

  1. recevoir le corps brut ;
  2. vérifier la signature ;
  3. valider les champs indispensables ;
  4. enregistrer l'événement de façon durable ;
  5. publier ou planifier son traitement ;
  6. répondre avec un statut de succès ;
  7. traiter l'événement dans un worker observable.

Pour un événement invalide ou non authentifié, retournez une erreur adaptée et n'enregistrez pas le message comme traité. Pour une panne temporaire après validation, conservez l'événement et rendez l'échec visible au système de reprise.

Ne pas dépendre de l'ordre d'arrivée

Un système distribué peut recevoir subscription.updated avant subscription.created, ou un état ancien après un état plus récent. Le réseau, les files et les mécanismes de retry rendent l'ordre d'arrivée différent de l'ordre de production.

Deux stratégies sont possibles selon le domaine :

  • traiter chaque événement comme une transition vérifiable, avec les préconditions nécessaires ;
  • considérer le webhook comme un signal et relire l'état actuel auprès de l'API source avant de modifier votre système.

La seconde approche est souvent plus sûre pour les entités dont l'état courant fait foi. Le webhook indique qu'une ressource a changé, puis votre application récupère cette ressource avec une authentification indépendante et synchronise un état connu.

Dans les deux cas, documentez la gestion des événements obsolètes. Un message ancien ne doit pas écraser un état plus récent simplement parce qu'il a été traité plus tard.

Ne vous contentez pas d'un tri par timestamp fourni dans le payload. Vérifiez la sémantique du fournisseur, la précision de l'horodatage et la possibilité de recevoir des messages provenant de plusieurs régions ou files.

Valider le contrat avant de valider le métier

La signature ne garantit pas que votre version de l'application comprend encore le payload.

Les fournisseurs ajoutent des champs, introduisent de nouveaux types d'événements ou modifient progressivement leur contrat. Votre code doit distinguer :

  • les champs obligatoires, sans lesquels le message est inutilisable ;
  • les champs optionnels, qui peuvent apparaître ou disparaître ;
  • les types d'événements connus ;
  • les types inconnus, qui doivent être journalisés et traités selon une politique explicite.

Validez la taille du corps, le type MIME, les champs attendus et les limites métier. Refusez les payloads anormalement volumineux avant de les transmettre à un parser ou à un modèle.

Pour les champs sensibles, ne faites pas confiance à une valeur uniquement parce qu'elle respecte un schéma. Vérifiez que la ressource appartient au bon compte, au bon environnement et au bon périmètre client. Un identifiant valide mais associé au mauvais tenant reste une faille d'autorisation.

Versionnez le contrat lorsque le fournisseur le permet et testez des exemples réels anonymisés. Un test avec uniquement le payload nominal ne couvre ni les champs manquants, ni les nouveaux types, ni les erreurs de fournisseur.

Distinguer les erreurs définitives des erreurs temporaires

Tous les échecs ne doivent pas déclencher le même comportement.

Une panne réseau vers votre base ou une API indisponible est généralement temporaire. Un payload dont la signature est invalide ou dont le contrat est impossible à comprendre est probablement définitif. Les deux doivent être enregistrés différemment.

Je distingue au minimum :

  • événement accepté, conservé et traité ;
  • événement déjà traité, renvoyé comme succès sans rejouer l'action ;
  • échec temporaire, planifié pour une nouvelle tentative ;
  • échec définitif, isolé dans une file d'erreur avec une raison claire ;
  • événement inconnu, conservé pour analyse sans déclencher d'effet de bord non prévu.

Une stratégie de retry doit prévoir un délai progressif, une limite ou une condition d'arrêt, et une file de quarantaine pour les messages qui ne peuvent pas être traités automatiquement.

Le retry doit porter sur l'événement et son état, pas seulement sur un appel HTTP qui a échoué. Sinon, vous perdez le contexte et rendez le diagnostic difficile.

Construire une observabilité utile

Un webhook qui retourne 200 n'est pas nécessairement un webhook qui fonctionne.

Pour chaque événement, vous devez pouvoir retrouver :

  • le fournisseur et l'identifiant de livraison ;
  • le type et la version du contrat ;
  • la date de réception ;
  • le résultat de la vérification de signature ;
  • le statut de validation ;
  • le nombre de tentatives internes ;
  • le traitement déclenché ;
  • le résultat et la durée ;
  • le motif d'un rejet ou d'une mise en quarantaine.

Utilisez un identifiant de corrélation commun entre le endpoint, la file, le worker et la logique métier. Mesurez séparément le délai de réception, le délai d'attente en file et le délai de traitement. Une moyenne seule masque les messages bloqués depuis longtemps.

Ne journalisez pas aveuglément le payload complet. Les événements peuvent contenir des données personnelles, des informations de paiement ou des secrets accidentellement transmis par un système tiers. Masquez les champs sensibles et définissez une durée de conservation.

Ajoutez des alertes sur les signaux réellement actionnables : hausse des signatures invalides, accumulation dans la file, augmentation des événements en échec, absence prolongée d'événements attendus ou divergence entre le fournisseur et votre base.

Tester le chemin nominal et les pannes

Avant de mettre un webhook en production, testez au moins les cas suivants :

  • signature valide et payload conforme ;
  • signature invalide ;
  • horodatage trop ancien ou réutilisé ;
  • événement identique reçu plusieurs fois ;
  • deux livraisons identiques reçues en parallèle ;
  • événement reçu dans le mauvais ordre ;
  • payload incomplet ou trop volumineux ;
  • type d'événement inconnu ;
  • timeout pendant le traitement métier ;
  • indisponibilité de la base ou d'un service externe ;
  • reprise d'un événement en échec ;
  • worker arrêté après l'enregistrement mais avant l'action ;
  • modification du contrat côté fournisseur.

Testez aussi ce qui se passe lorsque le fournisseur rejoue un événement depuis son interface. Un rejeu manuel doit être traçable et rester soumis aux mêmes règles d'idempotence qu'une livraison initiale.

Conservez un jeu de payloads synthétiques et anonymisés. Les tests ne doivent pas dépendre d'un compte client réel ni d'un secret de production.

La checklist d'un webhook prêt pour la production

Je ne considère pas une intégration webhook prête tant que les réponses suivantes ne sont pas claires :

  • Les requêtes sont-elles authentifiées par signature ou mécanisme équivalent ?
  • Le corps brut est-il conservé jusqu'à la vérification ?
  • Les secrets sont-ils absents du code, des URLs et des logs ?
  • L'identifiant de l'événement est-il stocké avec une contrainte d'unicité ?
  • Deux livraisons simultanées produisent-elles une seule action métier ?
  • L'événement est-il conservé avant l'accusé de réception ?
  • Les traitements longs sont-ils séparés de la réception HTTP ?
  • Les événements hors ordre et obsolètes sont-ils gérés explicitement ?
  • Les erreurs temporaires et définitives sont-elles distinguées ?
  • Existe-t-il une file de quarantaine et une procédure de rejeu ?
  • Les logs permettent-ils de reconstituer le traitement sans exposer de données sensibles ?
  • Les alertes indiquent-elles quand une intervention est nécessaire ?
  • Les tests couvrent-ils les doublons, les pannes, les injections et les changements de contrat ?

Si plusieurs réponses sont "on ne sait pas", le risque ne se trouve pas dans l'URL du webhook. Il se trouve dans le contrat implicite entre deux systèmes que personne ne surveille réellement.

FAQ : sécuriser et fiabiliser un webhook

Faut-il répondre 200 à un webhook déjà reçu ?

Oui, si l'identifiant de l'événement a été vérifié et que le message a déjà été traité avec succès. Renvoyer une erreur provoquerait souvent un nouveau retry inutile. Si le message a été enregistré mais reste en cours de traitement, votre modèle de données doit décider s'il faut confirmer la réception, reprendre le traitement ou signaler un état temporaire.

Un webhook signé est-il forcément sûr ?

Non. La signature vérifie l'origine et l'intégrité du message selon le mécanisme du fournisseur. Elle ne remplace pas les contrôles d'autorisation, la validation du contrat, l'idempotence, la protection contre le rejeu ni les règles métier.

Faut-il traiter le webhook immédiatement ?

La réception doit être immédiate, mais le traitement métier ne doit pas forcément être synchrone. Enregistrer le message puis le traiter dans une file ou un worker réduit les timeouts et rend les reprises plus fiables.

Comment éviter qu'un événement ancien écrase un événement récent ?

Ne dépendez pas uniquement de l'ordre d'arrivée. Comparez les versions ou dates d'état lorsque leur sémantique est fiable, ou relisez l'état courant auprès du fournisseur avant de synchroniser votre base. Refusez explicitement les transitions obsolètes.

Que faire lorsqu'un fournisseur ne propose pas de signature ?

Commencez par vérifier s'il existe un mécanisme d'authentification recommandé, comme mTLS, une authentification HTTP dédiée ou une liste de réseaux complétée par un secret. N'utilisez pas une URL difficile à deviner comme unique protection. Si aucun contrôle sérieux n'est disponible, limitez fortement les actions déclenchées et placez un intermédiaire capable de valider et de filtrer les messages.

Ce que je recommande, en tant que CTO hands-on

Un webhook est une frontière entre deux systèmes, pas une simple route à ajouter dans un framework. Sa fiabilité dépend autant du stockage, des transactions et de l'observabilité que du code qui parse le JSON.

Commencez par cartographier les événements qui peuvent produire une action irréversible. Ajoutez ensuite l'authentification, l'idempotence, la conservation durable et le traitement asynchrone. Ce socle vous donnera une intégration vérifiable avant de multiplier les fournisseurs et les automatisations.

C'est le type de sujet que je traite dans une mission d'intégration d'API externe ou d'audit technique : identifier les hypothèses invisibles, sécuriser les frontières et construire un workflow qui reste compréhensible lorsque le réseau, le fournisseur ou votre propre application tombe en panne.

Un webhook fiable n'est pas celui qui ne reçoit jamais d'erreur. C'est celui qui sait quoi faire quand le même message arrive deux fois, dans le désordre, après un timeout ou avec une version inattendue.

Sources et références

Benjamin Raimond

Benjamin Raimond

CTO hands-on : j'architecture, je code, je structure l'équipe technique. Je transforme des objectifs business en solutions robustes et durables pour les startups et PME.

En savoir plus →