Aller au contenu principal

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

Prochaines étapes