Wave Business webhook = critical for payment notifications. Without HMAC validation = fraud risk. Here's 2026 production security.
TL;DR
- Mandatory HMAC SHA-256.
- Idempotence: webhook received multiple times.
- Wave retry policy: 3 attempts over 24h.
- Critical audit logging.
HMAC validation
`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' }),
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 if event already processed
const existing = await db.waveEvents.findOne({ wave_event_id: event.id });
if (existing) {
console.log(Event ${event.id} already processed);
return;
}
// Optimistic lock
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(
Need a professional website?
Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 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 });
}
}
`
Wave configuration
`
- Go to business.wave.com → Webhooks
- Create endpoint:
- URL: https://kolonell.com/api/webhooks/wave
- Events: checkout.*, payout.*
- Signing secret: generated (store WAVE_WEBHOOK_SECRET)
- Test via dashboard
- Activate
`
Security tests
`typescript
// CI tests to run
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 delay?
A: 5 min, 30 min, 2h. 3 attempts total over 24h.
Q: How to recover missed events?
A: Reconcile endpoint: pull transactions API + match vs DB events.
Conclusion
2026 Wave webhook: HMAC + idempotence + retry + logging = production-ready. Critical CI security tests. Reconcile fallback for missed events.
Mohamed Bah
Fondateur, Kolonell
Passionate about digital and entrepreneurship in Africa, Mohamed has been helping Sénégalese businesses with their digital transformation since 2020. Founder of Kolonell, he believes every SME deserves a professional and accessible online présence.

