E-commerce9 min de lecture

Wave Business webhook signature : sécurité et idempotence pour développeurs (2026)

Mohamed Bah·Fondateur, Kolonell
2 juin 2026
Partager :
Wave Business webhook signature : sécurité et idempotence pour développeurs (2026)

Wave Business webhook signature : sécurité et idempotence pour développeurs (2026)

E-commerce

Pourquoi sécuriser les webhooks Wave Business est non-négociable

En 2026, Wave Business est devenu le rail de paiement dominant au Sénégal (estimé 65-70 % des paiements mobile-money e-commerce locaux). Quand un client paie sur votre site, Wave envoie un webhook HTTP POST vers votre endpoint pour vous notifier du statut (payment.completed, payment.failed, payment.cancelled).

Trois risques techniques majeurs guettent toute intégration webhook mal codée :

  • Spoofing : un attaquant envoie de faux webhooks à votre endpoint pour déclencher la livraison d'une commande non payée.
  • Replay attacks : un attaquant rejoue un webhook légitime intercepté pour créditer plusieurs fois la même commande.
  • Double-traitement : Wave retente automatiquement un webhook si votre endpoint a renvoyé un timeout ou un 5xx — sans idempotence, vous expédiez deux fois la même commande.

Ce guide couvre les trois défenses : vérification HMAC, fenêtre temporelle anti-replay, idempotence DB. Avec snippets Node.js (Next.js API route) et PHP (Laravel). Audience : devs e-commerce Sénégal qui intègrent Wave Business directement.

H2 : Vérification de la signature HMAC

Wave Business signe chaque webhook avec un secret partagé (récupéré dans le dashboard merchant sous API & Webhooks → Webhook secret). La signature est envoyée dans le header Wave-Signature au format :

`

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

`

Le calcul est : HMAC-SHA256(secret, timestamp + "." + rawBody). Toujours utiliser le raw body brut, jamais le JSON re-sérialisé (sinon la signature ne matche pas à cause des changements d'ordre de clés ou d'espaces).

Snippet Node.js (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 : fenêtre temporelle 5 min

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

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

// 2. Recalcul HMAC

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

const expected = crypto

.createHmac('sha256', WAVE_WEBHOOK_SECRET)

.update(payload)

.digest('hex');

// 3. Comparaison en temps constant (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 });

}

// ... idempotence + traitement métier (voir section suivante)

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

}

`

Snippet PHP (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');

// ... idempotence + traitement

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

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.

}

`

H2 : Idempotence en base de données

Wave retente un webhook jusqu'à 24h si vous renvoyez un 5xx ou si vous timeoutez (>10s). Si votre handler crédite la commande puis crashe avant de répondre 2xx, Wave réenverra — sans idempotence, vous expédiez deux fois.

Pattern recommandé : table webhook_events avec event_id UNIQUE.

`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()

);

`

Flux idempotent en pseudo-code :

`ts

const eventId = body.id; // ex: "evt_01HZK7Y..."

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

const inserted = await db.webhookEvent.upsert({

where: { eventId },

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

update: {}, // no-op si déjà vu

});

if (inserted.processedAt) {

// Déjà traité → ACK direct sans re-livrer

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

}

// Transaction : traiter + marquer 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() },

});

});

`

Pourquoi la transaction : si la mise à jour de la commande passe mais le marquage processedAt échoue, Wave retentera → vous retomberez dans la transaction → ON CONFLICT empêche le double-update.

H2 : Coûts et investissement intégration robuste

PosteCoûtRécurrent mensuel
Dév intégration webhook + idempotence (3-5 j)450 000 à 850 000 FCFA
Monitoring webhooks (Sentry, Better Stack)25 000 à 65 000 FCFA
Audit sécurité initial (revue code + pentest endpoint)280 000 à 650 000 FCFA
Documentation runbook équipe ops120 000 FCFA
Replay endpoint admin (refacturation incidents)180 000 à 380 000 FCFA

Total intégration robuste : 1,0-1,9 M FCFA one-shot + 25-65 K FCFA/mois. ROI : un seul incident de double-livraison sur une commande à 250 KFCFA paie déjà l'audit. Sur un site faisant 80-200 transactions/jour, l'absence d'idempotence se paye en quelques semaines.

H2 : Erreurs fréquentes vues sur des intégrations Sénégal

  • Re-sérialiser le JSON avant signature : 80 % des bugs de signature viennent de JSON.stringify(JSON.parse(body)) qui change l'ordre des clés.
  • Tolérance temporelle trop large : laisser >15 min ouvre la porte au replay. 5 min est le standard, alignez vos serveurs sur NTP.
  • Secret en clair dans le repo : utiliser .env.local (gitignored). Rotation tous les 6 mois minimum.
  • Pas de rate-limit : un endpoint webhook public sans rate-limit est une cible DoS. Limiter à 50 req/s par IP au reverse-proxy.
  • Logs sans rotation : payload JSON peut saturer le disque. Logger les eventId + statut, pas le payload complet en production.

FAQ

Que faire si le secret Wave fuit ?

Régénérer immédiatement le secret dans le dashboard Wave Business (API & Webhooks → Rotate secret). Déployer la nouvelle valeur en env vars avant rotation, sinon vous perdez des webhooks pendant la fenêtre de bascule. Wave supporte la double signature pendant 24h après rotation, profitez-en pour une bascule sans downtime.

Combien de temps Wave retente un webhook qui échoue ?

Wave retente avec backoff exponentiel pendant 24h : à 1 min, 5 min, 30 min, 2h, 6h, 12h, 24h. Après 24h, l'événement est marqué undelivered et reste consultable dans le dashboard. Toujours prévoir un endpoint admin /admin/webhooks/replay/:eventId pour rejouer manuellement les undelivered.

Faut-il vérifier la signature OU utiliser HTTPS suffit ?

HTTPS chiffre le transport (TLS) mais ne prouve pas que l'expéditeur est Wave. N'importe qui peut POST en HTTPS vers votre endpoint public. La signature HMAC est la seule preuve cryptographique d'origine. Les deux sont nécessaires, pas l'un OU l'autre.

Comment tester l'idempotence en dev ?

Utiliser curl pour replayer manuellement le même payload deux fois sur votre endpoint sandbox. Le deuxième appel doit retourner already_processed: true sans toucher à la commande. CI : ajouter un test e2e qui POST le même eventId 3 fois et vérifie que order.paidAt n'a pas bougé.

Webhook reçu mais commande introuvable en DB ?

Race condition classique : Wave envoie le webhook avant que votre frontend ait fini de créer la commande côté DB. Solution : créer la commande en statut pending AVANT d'appeler l'API Wave checkout. Le webhook update pending → paid. Jamais de webhook qui crée la commande from scratch.

Discutons de votre intégration

Si vous intégrez Wave Business sur votre e-commerce et voulez sécuriser proprement les webhooks (signature + idempotence + monitoring), nous pouvons auditer votre code et livrer une intégration robuste. WhatsApp +221 77 596 93 33.

Tags :#Wave Business#webhook#sécurité#HMAC#idempotence#Node.js#PHP
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.