E-commerce9 min read

Wave Business webhook signature: security and idempotency for developers (2026)

Mohamed Bah·Fondateur, Kolonell
June 2, 2026
Share:
Wave Business webhook signature: security and idempotency for developers (2026)

Wave Business webhook signature: security and idempotency for developers (2026)

E-commerce

Why Wave Business webhook security is non-negotiable

By 2026, Wave Business has become the dominant payment rail in Senegal (estimated 65-70% of local mobile-money e-commerce volume). When a customer pays on your site, Wave sends an HTTP POST webhook to your endpoint to notify you of the status (payment.completed, payment.failed, payment.cancelled).

Three major technical risks lurk in any poorly coded webhook integration:

  • Spoofing: an attacker sends fake webhooks to your endpoint to trigger fulfillment of an unpaid order.
  • Replay attacks: an attacker replays an intercepted legitimate webhook to credit the same order multiple times.
  • Double-processing: Wave automatically retries a webhook if your endpoint timed out or returned 5xx — without idempotency, you ship the same order twice.

This guide covers all three defenses: HMAC verification, anti-replay time window, DB idempotency. With Node.js (Next.js API route) and PHP (Laravel) snippets. Audience: back-end and full-stack devs integrating payments for the first time, anywhere in the world but particularly relevant for African e-commerce.

H2: HMAC signature verification

Wave Business signs every webhook with a shared secret (found in the merchant dashboard under API & Webhooks → Webhook secret). The signature is sent in the Wave-Signature header:

`

Wave-Signature: t=1717329600,v1=5257a869e7ecebeda32affa62cdca3fce648f....

`

The computation is: HMAC-SHA256(secret, timestamp + "." + rawBody). Always use the raw request body, never re-serialized JSON (otherwise the signature won't match due to key ordering or whitespace changes).

Node.js snippet (Next.js 14 API route)

`ts

// src/app/api/webhooks/wave/route.ts

import { NextRequest, NextResponse } from 'next/server';

import crypto from 'node:crypto';

const WAVE_WEBHOOK_SECRET = process.env.WAVE_WEBHOOK_SECRET!;

const TOLERANCE_SECONDS = 300; // 5 min

function verifyWaveSignature(rawBody: string, header: string): boolean {

const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));

const timestamp = parseInt(parts.t, 10);

const signature = parts.v1;

if (!timestamp || !signature) return false;

// 1. Anti-replay: 5-min time window

const now = Math.floor(Date.now() / 1000);

if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false;

// 2. HMAC recomputation

const payload = ${timestamp}.${rawBody};

const expected = crypto

.createHmac('sha256', WAVE_WEBHOOK_SECRET)

.update(payload)

.digest('hex');

// 3. Constant-time comparison (timing attack)

return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

}

export async function POST(req: NextRequest) {

const rawBody = await req.text();

const sig = req.headers.get('wave-signature') ?? '';

if (!verifyWaveSignature(rawBody, sig)) {

return NextResponse.json({ error: 'invalid_signature' }, { status: 401 });

}

// ... idempotency + business logic (see next section)

return NextResponse.json({ received: true });

}

`

PHP snippet (Laravel controller)

`php

public function handle(Request $request)

{

$raw = $request->getContent();

$header = $request->header('Wave-Signature', '');

parse_str(str_replace(',', '&', $header), $parts);

$ts = (int)($parts['t'] ?? 0);

$sig = $parts['v1'] ?? '';

if (abs(time() - $ts) > 300) abort(401, 'replay_window');

$expected = hash_hmac('sha256', $ts . '.' . $raw, env('WAVE_WEBHOOK_SECRET'));

if (!hash_equals($expected, $sig)) abort(401, 'bad_signature');

// ... idempotency + processing

return response()->json(['received' => true]);

Need a professional website?

Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.

}

`

H2: Database idempotency

Wave retries a webhook for up to 24h if you return 5xx or timeout (>10s). If your handler credits the order then crashes before responding 2xx, Wave will redeliver — without idempotency, you ship twice.

Recommended pattern: a webhook_events table with UNIQUE event_id.

`sql

CREATE TABLE webhook_events (

event_id VARCHAR(64) PRIMARY KEY,

provider VARCHAR(16) NOT NULL,

type VARCHAR(64) NOT NULL,

payload JSONB NOT NULL,

processed_at TIMESTAMPTZ,

created_at TIMESTAMPTZ DEFAULT NOW()

);

`

Idempotent flow in pseudo-code:

`ts

const eventId = body.id; // e.g.: "evt_01HZK7Y..."

// INSERT ... ON CONFLICT DO NOTHING (atomic)

const inserted = await db.webhookEvent.upsert({

where: { eventId },

create: { eventId, provider: 'wave', type: body.type, payload: body },

update: {}, // no-op if already seen

});

if (inserted.processedAt) {

// Already processed → ACK directly without re-delivering

return NextResponse.json({ already_processed: true });

}

// Transaction: process + mark processed_at

await db.$transaction(async (tx) => {

await tx.order.update({

where: { id: body.data.order_id },

data: { status: 'paid', paidAt: new Date() },

});

await tx.webhookEvent.update({

where: { eventId },

data: { processedAt: new Date() },

});

});

`

Why the transaction: if the order update succeeds but the processedAt marking fails, Wave will retry → you re-enter the transaction → ON CONFLICT prevents the duplicate update.

H2: Costs of a robust integration

ItemCostMonthly recurring
Webhook + idempotency dev (3-5 days)450,000 to 850,000 FCFA
Webhook monitoring (Sentry, Better Stack)25,000 to 65,000 FCFA
Initial security audit (code review + endpoint pentest)280,000 to 650,000 FCFA
Ops team runbook documentation120,000 FCFA
Admin replay endpoint (incident recovery)180,000 to 380,000 FCFA

Total robust integration: 1.0-1.9 M FCFA one-shot + 25-65 KFCFA/month. ROI: a single double-shipment incident on a 250 KFCFA order already pays for the audit. On a site doing 80-200 transactions/day, the absence of idempotency pays itself off in weeks.

H2: Common mistakes seen in field integrations

  • Re-serializing JSON before signing: 80% of signature bugs come from JSON.stringify(JSON.parse(body)) reshuffling keys.
  • Time tolerance too wide: leaving >15 min opens the door to replay. 5 min is the standard, sync your servers via NTP.
  • Plaintext secret in repo: use .env.local (gitignored). Rotate at least every 6 months.
  • No rate-limit: a public webhook endpoint without rate-limit is a DoS target. Limit to 50 req/s per IP at the reverse-proxy.
  • Unrotated logs: full JSON payload can saturate disks. Log eventId + status, not the full payload in production.

FAQ

What to do if the Wave secret leaks?

Immediately regenerate the secret in the Wave Business dashboard (API & Webhooks → Rotate secret). Deploy the new value to env vars BEFORE rotating, otherwise you lose webhooks during the swap window. Wave supports dual signatures for 24h after rotation — take advantage for a zero-downtime swap.

How long does Wave retry a failing webhook?

Wave retries with exponential backoff for 24h: at 1 min, 5 min, 30 min, 2h, 6h, 12h, 24h. After 24h, the event is marked undelivered and remains queryable in the dashboard. Always provide an admin endpoint /admin/webhooks/replay/:eventId to manually replay undelivered events.

Is HTTPS enough, or do I really need signature verification?

HTTPS encrypts transport (TLS) but doesn't prove the sender is Wave. Anyone can HTTPS POST to your public endpoint. HMAC signature is the only cryptographic origin proof. Both are required, not one OR the other.

How to test idempotency in dev?

Use curl to manually replay the same payload twice on your sandbox endpoint. The second call must return already_processed: true without touching the order. CI: add an e2e test that POSTs the same eventId 3 times and asserts order.paidAt didn't move.

Webhook received but order not found in DB?

Classic race condition: Wave sends the webhook before your frontend has finished creating the order in DB. Solution: create the order in pending status BEFORE calling the Wave checkout API. The webhook updates pending → paid. Never let a webhook create the order from scratch.

Let's discuss your integration

If you are integrating Wave Business on your e-commerce and want to properly secure webhooks (signature + idempotency + monitoring), we can audit your code and deliver a robust integration. WhatsApp +221 77 596 93 33.

Tags:#Wave Business#webhook#security#HMAC#idempotency#Node.js#PHP
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.