Websites6 min read

Wave Business webhook validation security: 2026

Mohamed Bah·Fondateur, Kolonell
June 20, 2026
Share:
Wave Business webhook validation security: 2026

Wave Business webhook validation security: 2026

Websites

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.

Tags:#Wave#Webhook#Security#HMAC#Production
Share:

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.