Sites Web6 min de lecture

Wave Business webhook validation sécurité : 2026

Mohamed Bah·Fondateur, Kolonell
20 juin 2026
Partager :
Wave Business webhook validation sécurité : 2026

Wave Business webhook validation sécurité : 2026

Sites Web

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.

Tags :#Wave#Webhook#Sécurité#HMAC#Production
Partager :

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.