Ce guide a été rédigé par notre équipe éditoriale et vérifié en croisant les sources officielles.En savoir plus sur notre méthode
- Version: Express 5.2.1 (MIT, gratuit) — Node.js 18+ requis
- Ce qu'il faut faire: cloner le repo, installer les dépendances, configurer.env et lancer le serveur
- Piège fréquent: oublier de vérifier la signature HMAC-SHA256 expose votre endpoint à n'importe quelle requête forgée
- Délai: 15 minutes de l'installation au premier webhook fonctionnel
Introduction
Vous allez construire un serveur webhook fonctionnel en 15 minutes avec Express 5.2.1 et Node.js 18+, capable de recevoir, vérifier et logger des payloads JSON depuis Stripe, GitHub ou tout autre service. Documentation officielle Express.
Ce que vous allez construire et pourquoi en 2026
Un serveur Express qui écoute les webhooks entrants, vérifie la signature HMAC-SHA256 de chaque requête, parse le payload JSON et le stocke dans un fichier log structuré. Prérequis: Node.js 18+ installé (node -v pour vérifier), 8 Go RAM minimum, macOS/Linux/Windows. Temps estimé: 15 minutes. Licence Express: MIT — gratuit et open-source.
Installation: commandes exactes à copier
mkdir webhook-receiver && cd webhook-receiver
npm init -y
npm install express@5.2.1
npm install --save-dev nodemon
Résultat attendu: added 2 packages dans node_modules/. Erreur courante: npm ERR! code EACCES → lancer sudo chown -R $(whoami) ~/.npm puis réessayer.
Configuration: les fichiers et variables à définir
Créer le fichier .env à la racine:
PORT=3000
WEBHOOK_SECRET=whsec_votre_cle_secrete_ici
LOG_DIR=./logs
Installer dotenv pour charger ces variables:
npm install dotenv
Chaque variable: PORT = port d'écoute du serveur (3000 par défaut), WEBHOOK_SECRET = clé secrète fournie par le service émetteur (Stripe, GitHub, etc.), LOG_DIR = répertoire où stocker les payloads reçus.
Le code du projet: construire la fonctionnalité principale
Créer server.js:
require('dotenv').config();
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const express = require('express'); const app = express();
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const LOG_DIR = process.env.LOG_DIR || './logs'; // Créer le répertoire de logs s'il n'existe pas
if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR, { recursive: true });
} // Middleware pour lire le body en raw buffer (nécessaire pour vérifier la signature)
app.use('/webhook', express.raw({ type: 'application/json' })); // Route webhook principale
app.post('/webhook', (req, res) => { const sig = req.headers['x-signature-256'] || req.headers['x-hub-signature-256'] || ''; const payload = req.body; // Vérification HMAC-SHA256 const expectedSig = crypto.createHmac('sha256', WEBHOOK_SECRET).update(payload).digest('hex'); if (sig!== 'sha256=' + expectedSig) { return res.status(401).json({ error: 'Signature invalide' }); } // Parser le payload JSON let event; try { event = JSON.parse(payload.toString()); } catch (e) { return res.status(400).json({ error: 'Payload JSON invalide' }); } // Logger l'événement dans un fichier horodaté const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const logFile = path.join(LOG_DIR, 'webhook-' + timestamp + '.json'); fs.writeFileSync(logFile, JSON.stringify(event, null, 2)); console.log('✓ Webhook reçu: ' + (event.type || 'type inconnu') + ' — sauvé dans ' + logFile); res.status(200).json({ received: true });
}); // Route santé
app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() });
}); app.listen(PORT, () => { console.log('Webhook receiver démarré sur le port ' + PORT);
});
Le code vérifie la signature HMAC-SHA256 de chaque requête entrante — sans cette vérification, n'importe qui peut envoyer des faux webhooks à votre endpoint. La comparaison sig!== 'sha256=' + expectedSig rejette les requêtes non signées. Le payload est loggé dans ./logs/ avec un nom de fichier horodaté.
Tester et valider que ça fonctionne
Lancer le serveur:
npx nodemon server.js
Envoyer un webhook de test avec curl:
SECRET="whsec_votre_cle_secrete_ici"
PAYLOAD='{"type":"test.event","data":{"message":"hello"}}'
SIG=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
curl -X POST http://localhost:3000/webhook \ -H "Content-Type: application/json" \ -H "x-signature-256: sha256=$SIG" \ -d "$PAYLOAD"
Résultat attendu dans le terminal: ✓ Webhook reçu: test.event — sauvé dans./logs/webhook-2026-05-13T.. Si vous recevez 401 Signature invalide, vérifiez que WEBHOOK_SECRET dans .env correspond exactement au secret utilisé pour générer la signature.
Vérifier la route santé: curl http://localhost:3000/health → {"status":"ok","uptime":.}
Déployer et utiliser en production
Sur un VPS (Ubuntu 22.04+), utiliser systemd pour garder le serveur actif:
sudo nano /etc/systemd/system/webhook-receiver.service
[Unit]
Description=Webhook Receiver Node.js
After=network.target [Service]
Type=simple
User=webhook
WorkingDirectory=/opt/webhook-receiver
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production [Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable webhook-receiver
sudo systemctl start webhook-receiver
sudo systemctl status webhook-receiver
Pour exposer le webhook derrière HTTPS, configurer Nginx en reverse proxy:
server { listen 443 ssl; server_name webhook.votre-domaine.fr; ssl_certificate /etc/letsencrypt/live/webhook.votre-domaine.fr/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/webhook.votre-domaine.fr/privkey.pem; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
}
Pour mettre à jour Express: npm update express puis sudo systemctl restart webhook-receiver.
Aller plus loin: 3 extensions concrètes
1. Ajouter un endpoint Stripe: utiliser stripe.webhooks.constructEvent() du package stripe (npm install stripe) pour vérifier automatiquement les signatures Stripe et traiter les événements invoice.paid, customer.created, etc.
2. Relayer vers Discord ou Slack: installer axios (npm install axios) et ajouter un axios.post() vers le webhook Discord/Slack dans la route /webhook pour recevoir une notification en temps réel à chaque événement.
3. Stocker en base PostgreSQL: installer pg (npm install pg) et remplacer le fs.writeFileSync par un INSERT INTO webhook_events(type, payload, received_at) pour interroger et filtrer les événements avec SQL.
Étapes à suivre5
Étape 1 — Installer Node.js et créer le projet
Vérifier que Node.js 18+ est installé:node -vdoit afficher v18.x ou supérieur. Si ce n'est pas le cas, installer Node.js via nvm:curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bashpuisnvm install 20. Recharger le shell:source ~/.bashrc. Vérifier:node -v && npm -vdoivent afficher les versions. Créer le répertoire du projet:mkdir webhook-receiver && cd webhook-receiver. Initialiser le package:npm init -y. Installer Express 5.2.1:npm install express@5.2.1. Installer les dépendances de développement:npm install --save-dev nodemon. Vérifier l'installation:node -e "const e=require("express"); console.log("Express chargé")"— doit afficher le message sans erreur. Ajouter le script de dev danspackage.json:"dev": "nodemon server.js". Cette étape prend 3 minutes.Étape 2 — Configurer les variables d'environnement
Créer le fichier.envà la racine du projet avec les 3 variables:PORT=3000(port d'écoute),WEBHOOK_SECRET=whsec_votre_cle_secrete_ici(remplacer par le secret fourni par Stripe, GitHub ou le service émetteur),LOG_DIR=./logs(répertoire de stockage des payloads). Installer dotenv:npm install dotenv. Charger dotenv en première ligne deserver.js:require('dotenv').config(). Créer le fichier.gitignoreet y ajouternode_modules/,.env,logs/— ne jamais committer le fichier.env contenant le secret. Pour Stripe, le secret se trouve dans le Dashboard > Developers > Webhooks > Signing secret. Pour GitHub, il se génère dans Settings > Webhooks > Add webhook > Secret. Ne jamais réutiliser le même secret pour deux services différents — chaque intégration doit avoir son propreWEBHOOK_SECRET. Créer le répertoire des logs:mkdir -p logs.Étape 3 — Construire le serveur webhook avec vérification HMAC
Créer le fichierserver.jsavec le code complet de la section principale ci-dessus. Les points clés du code: (1)express.raw({ type: 'application/json' })sur la route /webhook est obligatoire car la vérification HMAC nécessite le body brut, pas le body parsé — si vous utilisezexpress.json()avant la vérification, la signature sera toujours invalide. (2) La comparaisonsig!== 'sha256=' + expectedSigrejette toute requête sans signature valide. Pour une sécurité renforcée en production, utilisercrypto.timingSafeEqual()pour éviter les attaques par timing. (3) Chaque événement est loggé dans./logs/webhook-TIMESTAMP.jsonavec le payload complet pour audit et débogage. (4) La route/healthpermet de monitorer que le serveur est actif — utile pour les health checks Kubernetes ou les monitoring externes. Lancer le serveur:npx nodemon server.js— doit afficherWebhook receiver démarré sur le port 3000.Étape 4 — Tester avec curl et vérifier les signatures
Envoyer un webhook de test valide avec curl: générer la signature HMAC-SHA256 avecopenssl dgst -sha256 -hmac ""comme indiqué dans la section test ci-dessus. Résultat attendu:{"received":true}et un fichier JSON dans./logs/. Tester un webhook invalide (mauvaise signature): envoyer une requête sans le headerx-signature-256→ doit retourner401 {"error":"Signature invalide"}. Tester un payload invalide: envoyer du texte non-JSON avec une signature valide → doit retourner400 {"error":"Payload JSON invalide"}. Vérifier le fichier log:cat./logs/webhook-*.json | head -5— doit contenir le payload envoyé avec l'horodatage. Si la signature ne passe jamais: vérifier que leWEBHOOK_SECRETdans.env correspond exactement à celui utilisé pour générer la signature (pas d'espaces, pas de guillemets en trop). Utiliserecho -nau lieu deechopour éviter le saut de ligne final qui invalide le HMAC.Étape 5 — Déployer en production avec systemd et Nginx
Sur un VPS Ubuntu 22.04+: (1) Copier le projet:rsync -avz./webhook-receiver/ user@vps:/opt/webhook-receiver/. (2) Installer les dépendances:cd /opt/webhook-receiver && npm install --production. (3) Créer le service systemd comme indiqué dans la section déploiement ci-dessus. (4) Activer et démarrer:sudo systemctl enable webhook-receiver && sudo systemctl start webhook-receiver. (5) Configurer Nginx en reverse proxy avec HTTPS (Let's Encrypt) comme indiqué ci-dessus. (6) Ouvrir le port 443:sudo ufw allow 443. (7) Tester:curl https://webhook.votre-domaine.fr/healthdoit retourner{"status":"ok"}. Pour mettre à jour:git pull && npm install --production && sudo systemctl restart webhook-receiver. Configurer la rotation des logs: ajouter une crontabfind /opt/webhook-receiver/logs -name "*.json" -mtime +30 -deletepour supprimer les logs de plus de 30 jours.
Conseils pratiques
- Utilisez crypto.timing Safe Equal() au lieu de !== pour comparer les signatures HMAC — cela protège contre les attaques par timing side-channel. Remplacez la comparaison par: const sigBuf = Buffer.from(sig); const expectedBuf = Buffer.from('sha256=' + expectedSig); return crypto.timing Safe Equal(sig Buf, expectedBuf).
- Pour GitHub webhooks, le header est x-hub-signature-256 (pas x-signature-256) — le code ci-dessus gère les deux, mais si vous n'acceptez que GitHub, hardcodez le bon header pour éviter les ambiguïtés.
- Ajoutez app.use('/webhook', express.raw({ type: '*/*' })) si vous recevez des webhooks en form-urlencoded (Stripe envoie parfois du application/x-www-form-urlencoded). Pour Stripe spécifiquement, utilisez toujours express.raw({ type: 'application/json' }).
Points d'attention
- Erreur TypeError: Cannot read properties of undefined (reading 'type'): le body n'a pas été parsé en JSON. Vérifiez que express.raw() est bien appliqué sur /webhook AVANT le parsing manuel avec JSON.parse(). N'ajoutez PAS express.json() globalement — cela consomme le body brut et rend la vérification HMAC impossible.
- Erreur 401 Signature invalide à chaque requête: le WEBHOOK_SECRET dans .env ne correspond pas au secret utilisé pour signer. Pour Stripe, sélectionnez 'Reveal' à côté du Signing secret dans le Dashboard. Pour GitHub, régénérez le secret dans Settings > Webhooks. Ne mélangez pas les secrets entre services.
- Erreur EADDRINUSE: address already in use:::3000: un autre processus occupe le port. Tuez-le avec lsof -ti:3000 | xargs kill -9 ou changez le PORT dans .env. En production, configurez systemd avec Restart=on-failure pour éviter les conflits au redémarrage.
Questions fréquentes3
Quest-ce quun webhook et comment fonctionne-t-il ?
Un webhook est une requête HTTP POST envoyée automatiquement par un service quand un événement se produit. Par exemple, Stripe envoie un webhook quand un paiement est réussi. Votre serveur reçoit et traite cette notification.
Comment sécuriser un endpoint webhook ?
Vérifiez la signature HMAC-SHA256 dans le header X-Signature. Utilisez un secret partagé entre lémetteur et votre serveur. Ajoutez aussi un rate limiting et une validation du payload avec un schema Joi ou Zod.
Comment tester un webhook en local ?
Utilisez ngrok ou smee.io pour créer un tunnel entre votre serveur local et internet. Les services comme Stripe, GitHub ou Slack offrent aussi des outils de test dans leur dashboard.
Guides Technologie & IA
Voir toutMistral AI 2026: comment utiliser Le Chat et Vibe
Construire un chatbot local avec Ollama et Python en 30 minutes
Pixels de suivi dans les emails en 2026 : détecter, bloquer et protéger sa vie privée
Utiliser DeepSeek V4 en 2026 pour améliorer la recherche
Utiliser Claude Opus 4.7 en 2026 pour améliorer la productivité
Poursuivez votre lecture
Multi-catégoriesComment déclarer une pension de retraite étrangère aux impôts en France en 2026
Comment faire déclaration revenus expatrié
Comment déclarer comptes bancaires étrangers en 2026
"Impôts du Cœur" : les agents des finances publiques proposent leur aide à la déclaration de revenus sur le marché
Comment refuser un loyer supérieur à 30 % du revenu en 2026 ?
Transmission d'entreprise 2026 : dispositifs fiscaux et étapes clés pour vendre ou céder son affaire
Équipe éditoriale GuidePratiquefr
Rédacteurs spécialisés en droit, fiscalité et finances
Rédigé et vérifié par notre équipe de rédacteurs spécialisés. Sources officielles consultées : service-public.fr, legifrance.gouv.fr, impots.gouv.fr, ameli.fr. Dernière vérification : 3 juin 2026.
Méthodologie de vérification :notre charte éditoriale
Sources officielles consultées
Les informations de ce guide sont recoupées avec les sources officielles suivantes :
Les informations contenues dans ce guide sont fournies à titre indicatif et ne remplacent pas un conseil professionnel personnalisé. Consultez toujours le site officiel de l'administration concernée pour vérifier les informations en vigueur.