Webhooks expliqués : payloads signés et retries
Vous venez d'intégrer une passerelle de paiement. La doc dit : « Nous enverrons une requête POST à votre endpoint lorsqu'un paiement réussit. » Facile, non ? Vous créez une route simple qui accepte du JSON, met à jour votre base de données et renvoie 200. Puis ça arrive : le paiement d'un client réussit, mais votre serveur est brièvement indisponible. Le webhook n'arrive jamais. Pire encore, il arrive deux fois, et vous créditez le compte en double. Les webhooks semblent simples jusqu'à ce qu'ils échouent.
Cet article explique comment les webhooks fonctionnent réellement, pourquoi les payloads signés sont essentiels pour la sécurité, et comment construire une logique de retry qui garde votre intégration fiable—même quand les choses tournent mal.
Qu'est-ce qu'un webhook ?
Un webhook est un callback HTTP : au lieu que votre application interroge une API pour détecter des changements, le fournisseur envoie un POST HTTP vers une URL que vous spécifiez lorsqu'un événement se produit. C'est du push plutôt que du pull.
Exemples courants : confirmations de paiement (Stripe, PayPal), notifications de build CI/CD (GitHub, GitLab), et plateformes de messagerie (Slack, Discord). Le schéma est simple : vous enregistrez une URL, le fournisseur envoie des payloads JSON à cette URL, et votre serveur les traite.
Mais cette simplicité cache deux défis critiques : l'authenticité (comment savoir que la requête vient vraiment du fournisseur ?) et la fiabilité (que se passe-t-il si votre endpoint est indisponible ou lent ?).
Pourquoi les payloads signés sont importants
Votre endpoint webhook est une URL publique. N'importe qui peut lui envoyer une requête POST. Sans vérification, un attaquant pourrait forger un événement de paiement réussi, déclencher une expédition, ou corrompre vos données.
Les payloads signés résolvent ce problème en incluant une signature cryptographique dans les en-têtes de la requête. Le fournisseur calcule un HMAC (code d'authentification de message basé sur un hash) en utilisant un secret partagé et le corps de la requête, puis l'envoie avec le payload. Votre serveur recalcule le HMAC et le compare. S'ils correspondent, la requête est authentique.
Comment fonctionne la signature HMAC
- Le fournisseur et vous partagez une clé secrète (fournie lors de la configuration).
- Pour chaque webhook, le fournisseur concatène le timestamp et le corps brut de la requête, puis calcule un hash HMAC-SHA256 avec le secret.
- La signature est envoyée dans un en-tête, souvent quelque chose comme
X-SignatureouStripe-Signature. - Votre serveur recalcule le HMAC avec le même secret et le compare à la signature reçue.
- S'ils correspondent, vous traitez l'événement ; sinon, vous le rejetez avec un 401.
Utilisez toujours le corps brut de la requête pour le calcul du HMAC. Si votre framework parse le JSON avant la vérification, le corps peut être altéré (espaces, ordre des clés), ce qui fait échouer la vérification.
Exemple : vérifier une signature webhook en Node.js
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body for signature verification
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body; // Buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload.toString());
// ... handle event
res.status(200).send('OK');
});
Notez l'utilisation de timingSafeEqual pour prévenir les attaques temporelles. N'utilisez jamais une simple comparaison === pour les signatures.
Concevoir une logique de retry
Même avec les signatures, les webhooks peuvent échouer. Votre serveur peut être indisponible, le réseau peut perdre la requête, ou votre traitement peut expirer. Les fournisseurs réessaient généralement les livraisons échouées, mais vous devez aussi concevoir votre système pour gérer les retries avec élégance.
Qu'est-ce qui compte comme un échec ?
La plupart des fournisseurs considèrent toute réponse autre que 2xx comme un échec. Cela inclut les redirections 3xx, les erreurs client 4xx, et les erreurs serveur 5xx. Certains imposent aussi un timeout (par ex., 5 secondes). Si vous ne répondez pas rapidement, la livraison est marquée comme échouée et réessayée.
Backoff exponentiel avec jitter
Les fournisseurs réessaient généralement avec un backoff exponentiel : premier retry après 1 minute, puis 5 minutes, 30 minutes, etc. Ajouter du jitter (variation aléatoire) évite les effets de troupeau si de nombreux webhooks échouent simultanément.
Si vous construisez un émetteur de webhooks, implémentez le backoff comme ceci :
- Retry 1 : après 10 secondes
- Retry 2 : après 1 minute
- Retry 3 : après 5 minutes
- Retry 4 : après 30 minutes
- Retry 5 : après 2 heures
Après un nombre maximum de tentatives, déplacez l'événement vers une dead-letter queue pour inspection manuelle.
Idempotence : gérer les livraisons en double
Les retries signifient que votre endpoint peut recevoir le même événement plusieurs fois. Votre traitement doit être idempotent : traiter le même événement deux fois doit avoir le même effet que le traiter une fois.
Stratégies courantes :
- Déduplication par ID d'événement : stockez les IDs d'événements traités dans une base de données ou un cache. Avant de traiter, vérifiez si l'ID a déjà été vu.
- Upsert au lieu d'insert : utilisez des opérations de base de données qui gèrent naturellement les doublons (par ex.,
INSERT ... ON CONFLICT DO NOTHING). - Clés d'idempotence : pour des opérations comme le débit d'une carte, passez une clé unique pour que le fournisseur de paiement ignore les requêtes en double.
Bonnes pratiques pour les consommateurs de webhooks
| Pratique | Pourquoi c'est important |
|---|---|
| Vérifier les signatures | Empêche les requêtes forgées |
| Répondre rapidement (en moins de 2s) | Évite les timeouts et les retries inutiles |
| Traiter en asynchrone | Découple la réception du traitement lourd |
| Utiliser des handlers idempotents | Sécurise contre les livraisons en double |
| Logger tous les événements | Facilite le débogage et l'audit |
| Surveiller les échecs | Détecte les problèmes avant qu'ils s'aggravent |
Pensez aussi à valider le timestamp dans la signature pour prévenir les attaques par rejeu. De nombreux fournisseurs incluent un timestamp dans le payload signé ; rejetez les événements datant de plus de quelques minutes.
Bonnes pratiques pour les producteurs de webhooks
Si vous construisez un service qui envoie des webhooks, suivez ces recommandations :
- Signez chaque payload avec HMAC-SHA256.
- Incluez un ID d'événement unique et un timestamp.
- Réessayez avec un backoff exponentiel et du jitter.
- Fournissez un moyen aux consommateurs de consulter les logs de livraison et de rejouer manuellement les événements.
- Documentez clairement votre processus de vérification de signature.
FAQ
Qu'est-ce qu'un payload webhook signé ?
Un payload signé inclut une signature cryptographique (généralement HMAC) calculée à partir du corps de la requête et d'un secret partagé. Le destinataire vérifie la signature pour s'assurer que la requête est authentique et non modifiée.
Combien de fois un webhook doit-il être réessayé ?
Il n'y a pas de nombre universel, mais la plupart des fournisseurs réessaient 3 à 5 fois sur plusieurs heures. Après cela, l'événement est généralement marqué comme échoué et peut être disponible pour un rejeu manuel. Votre système doit gérer les doublons de manière idempotente quel que soit le nombre de retries.
Puis-je utiliser des webhooks sans HTTPS ?
Techniquement oui, mais vous devriez toujours utiliser HTTPS. Les webhooks contiennent souvent des données sensibles, et sans TLS, un attaquant pourrait intercepter ou altérer le payload. HTTPS garantit aussi que vous envoyez vers le serveur prévu.
Déboguer les webhooks avec les logs Nginx
Quand les webhooks échouent, le premier endroit à consulter est les logs de votre serveur web. Si vous utilisez Nginx, les logs d'accès et d'erreur enregistrent chaque requête, y compris les codes de statut et les temps de réponse. Vous pouvez rapidement repérer des patterns : 401 répétés (échec de vérification de signature), 500 (erreurs applicatives), ou timeouts.
Pour une analyse plus rapide, essayez le Nginx Log Analyzer pour parser et résumer le trafic webhook, filtrer par code de statut, et identifier les endpoints problématiques.