Bonnes pratiques webhooks
Guide des meilleures pratiques pour implémenter des webhooks fiables et performants.
1. Idempotence
Les webhooks peuvent être livrés plusieurs fois. Votre code doit être idempotent : traiter le même événement plusieurs fois doit produire le même résultat.
Solution : Tracker les événements traités
async function processWebhook(event) {
// Vérifier si déjà traité
const exists = await db.processedEvents.findOne({
eventId: event.id
});
if (exists) {
console.log(`Événement ${event.id} déjà traité`);
return { status: 'already_processed' };
}
// Traiter dans une transaction
const session = await db.startSession();
await session.withTransaction(async () => {
// Faire le traitement
await processEvent(event);
// Marquer comme traité
await db.processedEvents.create({
eventId: event.id,
processedAt: new Date(),
eventType: event.type
});
});
return { status: 'processed' };
}
Alternative : Utiliser une table de déduplication
CREATE TABLE webhook_events (
id VARCHAR(255) PRIMARY KEY,
event_type VARCHAR(100) NOT NULL,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_event (id)
);
-- Insertion idempotente
INSERT IGNORE INTO webhook_events (id, event_type)
VALUES ('evt_abc123', 'appointment.created');
2. Traitement asynchrone
Répondez rapidement (< 5s) et traitez en arrière-plan.
❌ Mauvais : Traitement synchrone
app.post('/webhooks', async (req, res) => {
const event = req.body;
// Traitement long qui bloque la réponse
await sendEmail(event); // 2s
await updateDatabase(event); // 3s
await callExternalAPI(event); // 4s
// Total: 9s → TIMEOUT !
res.status(200).send('OK');
});
✅ Bon : Queue asynchrone
const Bull = require('bull');
const webhookQueue = new Bull('webhooks');
app.post('/webhooks', async (req, res) => {
const event = req.body;
// Répondre immédiatement
res.status(200).send('OK');
// Ajouter à la queue
await webhookQueue.add(event, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
});
// Worker séparé
webhookQueue.process(async (job) => {
const event = job.data;
await sendEmail(event);
await updateDatabase(event);
await callExternalAPI(event);
});
Alternatives de queue
- Redis + Bull (Node.js)
- Celery (Python)
- Sidekiq (Ruby)
- Laravel Queues (PHP)
- RabbitMQ / AWS SQS (Tous langages)
3. Gestion des erreurs
Retry avec backoff exponentiel
async function processWebhookWithRetry(event, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
await processEvent(event);
console.log(`✓ Événement ${event.id} traité`);
return;
} catch (error) {
console.error(`✗ Tentative ${attempt}/${maxAttempts}:`, error.message);
if (attempt < maxAttempts) {
// Backoff exponentiel: 2s, 4s, 8s
const delay = Math.pow(2, attempt) * 1000;
await sleep(delay);
} else {
// Dernière tentative échouée
await logFailedWebhook(event, error);
throw error;
}
}
}
}
Dead Letter Queue
Pour les événements qui échouent définitivement :
async function handleFailedWebhook(event, error) {
await db.failedWebhooks.create({
eventId: event.id,
eventType: event.type,
payload: event,
error: error.message,
failedAt: new Date(),
attempts: 3
});
// Alerter l'équipe
await alertTeam({
title: `Webhook ${event.type} échoué`,
eventId: event.id,
error: error.message
});
}
4. Validation du payload
Validez toujours le format du payload avant traitement :
const Joi = require('joi');
const webhookSchema = Joi.object({
id: Joi.string().required(),
type: Joi.string().valid(
'appointment.created',
'appointment.updated',
'appointment.cancelled',
'appointment.validated',
'appointment.completed'
).required(),
created: Joi.date().iso().required(),
data: Joi.object({
object: Joi.object().required()
}).required()
});
app.post('/webhooks', async (req, res) => {
const event = req.body;
// Valider le schéma
const { error } = webhookSchema.validate(event);
if (error) {
console.error('Payload invalide:', error.details);
return res.status(400).send('Invalid payload');
}
// Vérifier la signature
if (!verifySignature(req)) {
return res.status(401).send('Invalid signature');
}
res.status(200).send('OK');
processWebhookAsync(event);
});
5. Monitoring et alertes
Métriques à suivre
const metrics = {
webhooksReceived: new Counter('webhooks_received_total'),
webhooksProcessed: new Counter('webhooks_processed_total'),
webhooksFailed: new Counter('webhooks_failed_total'),
processingTime: new Histogram('webhook_processing_seconds'),
};
app.post('/webhooks', async (req, res) => {
const start = Date.now();
metrics.webhooksReceived.inc();
try {
await processWebhook(req.body);
metrics.webhooksProcessed.inc();
} catch (error) {
metrics.webhooksFailed.inc();
throw error;
} finally {
const duration = (Date.now() - start) / 1000;
metrics.processingTime.observe(duration);
}
res.status(200).send('OK');
});
Logging structuré
const winston = require('winston');
const logger = winston.createLogger({
format: winston.format.json(),
transports: [new winston.transports.File({ filename: 'webhooks.log' })]
});
app.post('/webhooks', (req, res) => {
const event = req.body;
logger.info('Webhook received', {
eventId: event.id,
eventType: event.type,
deliveryId: req.headers['x-moncreneau-delivery'],
timestamp: new Date().toISOString()
});
// Traiter...
});
6. Tests
Tests unitaires
const request = require('supertest');
const crypto = require('crypto');
describe('Webhook Endpoint', () => {
it('should accept valid webhook', async () => {
const payload = JSON.stringify({
id: 'evt_test',
type: 'appointment.created',
data: { object: { id: 123 } }
});
const signature = 'sha256=' + crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
const response = await request(app)
.post('/webhooks/moncreneau')
.set('Content-Type', 'application/json')
.set('X-Moncreneau-Signature', signature)
.send(payload);
expect(response.status).toBe(200);
});
it('should reject invalid signature', async () => {
const response = await request(app)
.post('/webhooks/moncreneau')
.set('X-Moncreneau-Signature', 'sha256=invalid')
.send({ id: 'evt_test' });
expect(response.status).toBe(401);
});
});
Tests d'intégration avec ngrok
En développement local :
# Terminal 1: Démarrer votre serveur
npm start
# Terminal 2: Exposer avec ngrok
ngrok http 3000
# Terminal 3: Configurer le webhook
curl -X POST https://mc-prd.duckdns.org/api/v1/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"url": "https://abc123.ngrok.io/webhooks/moncreneau",
"events": ["appointment.created"]
}'
# Créer un RDV de test
curl -X POST https://mc-prd.duckdns.org/api/v1/appointments \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"departmentId": 5, "dateTime": "2026-02-01T10:00:00", "name": "Test"}'
7. Sécurité
Rate limiting
Protégez votre endpoint contre les abus :
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // 100 requêtes max
message: 'Too many webhook requests',
standardHeaders: true,
legacyHeaders: false,
});
app.post('/webhooks/moncreneau', webhookLimiter, handleWebhook);
IP Whitelisting
const allowedIPs = ['41.223.45.0/24', '41.223.46.0/24'];
function checkIPWhitelist(req, res, next) {
const clientIP = req.ip;
if (!isIPAllowed(clientIP, allowedIPs)) {
return res.status(403).send('Forbidden');
}
next();
}
app.post('/webhooks/moncreneau', checkIPWhitelist, handleWebhook);
8. Documentation interne
Documentez votre implémentation :
# Webhooks moncreneau
## Configuration
- URL: https://api.example.com/webhooks/moncreneau
- Secret: Stocké dans `MONCRENEAU_WEBHOOK_SECRET`
- Événements: appointment.created, appointment.cancelled
## Architecture
1. Réception → Validation signature
2. Réponse 200 immédiate
3. Queue Redis (Bull)
4. Worker traite async
## Monitoring
- Grafana dashboard: /dashboards/webhooks
- Alertes Slack: #alerts-webhooks
- Logs: CloudWatch /webhooks/moncreneau
## Dépannage
- Dead letter queue: `failedWebhooks` collection
- Rejeu manuel: `npm run replay-webhook <eventId>`
Checklist
Avant de passer en production :
- Signature HMAC vérifiée
- Traitement asynchrone implémenté
- Idempotence garantie (déduplication)
- Gestion d'erreurs + retry logic
- Validation du payload
- Logging structuré
- Métriques et alertes
- Tests unitaires et d'intégration
- HTTPS en production
- Documentation à jour