Webhook Wave Business = critique pour notification paiements. Sans validation HMAC = risque fraude. Voici sécurisation production 2026.
TL;DR
- HMAC SHA-256 obligatoire.
- Idempotence : webhook reçu plusieurs fois.
- Retry policy Wave : 3 tentatives sur 24h.
- Logging audit critique.
Validation HMAC
`typescript
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.WAVE_WEBHOOK_SECRET!;
function verifyWaveWebhook(rawBody: Buffer, signature: string): boolean {
const computed = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(signature)
);
}
// Express handler
app.post(
'/webhooks/wave',
express.raw({ type: 'application/json' }), // raw body for HMAC
async (req, res) => {
const signature = req.headers['wave-signature'] as string;
if (!signature) {
console.warn('Wave webhook: missing signature');
return res.status(401).end();
}
if (!verifyWaveWebhook(req.body, signature)) {
console.warn('Wave webhook: invalid signature');
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
await handleWaveEvent(event);
res.status(200).json({ received: true });
}
);
`
Idempotence
`typescript
async function handleWaveEvent(event: WaveEvent) {
// Check si event déjà traité
const existing = await db.waveEvents.findOne({ wave_event_id: event.id });
if (existing) {
console.log(Event ${event.id} already processed);
return;
}
// Lock optimiste
try {
await db.waveEvents.insertOne({
wave_event_id: event.id,
type: event.type,
data: event.data,
processed: false,
created_at: new Date(),
});
} catch (err) {
if (err.code === 11000) {
// Duplicate key (race condition)
return;
}
throw err;
}
// Process event
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutCompleted(event.data);
break;
case 'checkout.session.expired':
await handleCheckoutExpired(event.data);
break;
case 'payout.completed':
await handlePayoutCompleted(event.data);
break;
default:
console.warn(Unknown event type: ${event.type});
}
// Mark processed
await db.waveEvents.updateOne(
Besoin d'un site web professionnel ?
Kolonell crée des sites web qui attirent des clients, optimisés pour le marché sénégalais. Devis gratuit en 2 minutes.
{ wave_event_id: event.id },
{ processed: true, processed_at: new Date() }
);
}
`
Retry policy + logging
`typescript
async function handleWaveEventWithRetry(event: WaveEvent, attempt = 1): Promise
const MAX_ATTEMPTS = 3;
try {
await handleWaveEvent(event);
} catch (err) {
console.error(Webhook handling failed (attempt ${attempt}/${MAX_ATTEMPTS}):, err);
await db.waveEventErrors.insertOne({
wave_event_id: event.id,
attempt,
error: err.message,
stack: err.stack,
created_at: new Date(),
});
if (attempt < MAX_ATTEMPTS) {
await sleep(Math.pow(2, attempt) * 1000); // exponential backoff
return handleWaveEventWithRetry(event, attempt + 1);
}
// Final failure : alert + dead letter queue
await alertAdmin('Wave webhook final failure', { event, err });
await db.waveDeadLetter.insertOne({ event, errors_count: MAX_ATTEMPTS });
}
}
`
Configuration Wave
`
- Aller business.wave.com → Webhooks
- Créer endpoint :
- URL : https://kolonell.com/api/webhooks/wave
- Events : checkout.*, payout.*
- Signing secret : généré (à stocker WAVE_WEBHOOK_SECRET)
- Test via dashboard
- Activer
`
Tests sécurité
`typescript
// Tests à exécuter en CI
import { test } from 'vitest';
test('Reject webhook without signature', async () => {
const res = await fetch('http://localhost:3000/webhooks/wave', {
method: 'POST',
body: JSON.stringify({ type: 'checkout.session.completed' }),
});
expect(res.status).toBe(401);
});
test('Reject webhook with invalid signature', async () => {
const res = await fetch('http://localhost:3000/webhooks/wave', {
method: 'POST',
headers: { 'wave-signature': 'invalid' },
body: JSON.stringify({ type: 'checkout.session.completed' }),
});
expect(res.status).toBe(401);
});
test('Accept valid webhook', async () => {
const body = JSON.stringify({ type: 'checkout.session.completed' });
const signature = crypto
.createHmac('sha256', TEST_SECRET)
.update(body)
.digest('hex');
const res = await fetch('http://localhost:3000/webhooks/wave', {
method: 'POST',
headers: {
'wave-signature': signature,
'Content-Type': 'application/json',
},
body,
});
expect(res.status).toBe(200);
});
`
FAQ
Q : Wave retry délai ?
R : 5 min, 30 min, 2h. 3 tentatives total sur 24h.
Q : Comment recover events manqués ?
R : Endpoint reconcile : pull transactions API + match vs DB events.
Conclusion
Webhook Wave 2026 : HMAC + idempotence + retry + logging = production-ready. Tests sécurité CI critiques. Reconcile fallback pour events manqués.
Mohamed Bah
Fondateur, Kolonell
Passionné par le digital et l'entrepreneuriat en Afrique, Mohamed accompagne les entreprises sénégalaises dans leur transformation digitale depuis 2020. Fondateur de Kolonell, il croit que chaque PME mérite une présence en ligne professionnelle et accessible.

