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.no_show',
'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​