La majorité des PME sénégalaises qui intègrent Wave dans leur site copient un bout de doc, posent un endpoint, et signent leur tranquillité d'esprit. Trois mois plus tard, le premier double-paiement arrive : l'utilisateur a recliqué, le webhook s'est déclenché deux fois, et la commande a été expédiée deux fois. Ce guide pose la version production-ready.
TL;DR
- Le webhook Wave Business n'est pas idempotent par défaut : à vous de gérer le déduplicage côté serveur.
- Vérifiez TOUJOURS la signature HMAC avant d'écrire en base.
- Stockez l'
event_idWave dans une tablepayment_eventsavec contrainte unique : seconde réception = no-op.- Délai de retry Wave : 5 tentatives sur 24h, backoff exponentiel. Renvoyez 200 OK même sur événement déjà traité.
Architecture cible
`
[Wave POS / Wave App] → [Wave Backend] → POST /api/webhooks/wave
↓
- Vérifier HMAC signature
- Parse JSON
- Check event_id en DB → idempotent ?
- Si nouveau → écrire payment + commit
- Toujours répondre 200 OK
`
Étape 1 — endpoint Next.js App Router
`ts
// app/api/webhooks/wave/route.ts
import crypto from 'crypto';
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
const WAVE_WEBHOOK_SECRET = process.env.WAVE_WEBHOOK_SECRET!;
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get('wave-signature') ?? '';
// 1. Vérification HMAC
const expected = crypto
.createHmac('sha256', WAVE_WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return NextResponse.json({ error: 'invalid_signature' }, { status: 401 });
}
const event = JSON.parse(rawBody);
// 2. Idempotence : check event_id
const existing = await prisma.paymentEvent.findUnique({
where: { externalId: event.id },
});
if (existing) return NextResponse.json({ ok: true, deduped: true });
// 3. Persister atomiquement
await prisma.$transaction([
prisma.paymentEvent.create({
data: {
externalId: event.id,
provider: 'wave',
type: event.type,
payload: event,
},
}),
prisma.order.update({
where: { id: event.metadata.order_id },
data: { status: 'paid', paidAt: new Date() },
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.
}),
]);
return NextResponse.json({ ok: true });
}
`
Étape 2 — table payment_events (Prisma)
`prisma
model PaymentEvent {
id String @id @default(cuid())
externalId String @unique // event.id Wave
provider String // "wave" | "orange-money" | "stripe"
type String // "checkout.session.completed" etc.
payload Json
receivedAt DateTime @default(now())
@@index([provider, type])
}
`
La contrainte @unique sur externalId est votre filet de sécurité : même si la logique applicative laisse passer un doublon, Postgres rejettera l'insert.
Étape 3 — tester sans Wave en sandbox
Wave ne fournit pas d'environnement de test public stable au Sénégal en mai 2026. Deux options :
Option A — fixture locale + curl :
`bash
SECRET="votre_secret_local"
BODY='{"id":"evt_test_123","type":"merchant.payment_received","amount":15000,"currency":"XOF","metadata":{"order_id":"ord_42"}}'
SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST http://localhost:3000/api/webhooks/wave \
-H "Content-Type: application/json" \
-H "wave-signature: $SIG" \
-d "$BODY"
`
Option B — ngrok + numéro de test Wave réel : créez une commande à 100 FCFA, payez, vérifiez la trace en DB.
Erreurs classiques (audit Kolonell sur 23 sites en 2025)
| # | Erreur | Conséquence | Fix |
|---|---|---|---|
| 1 | Pas de vérification HMAC | Endpoint exposé, faux paiements possibles | Vérifier la signature avant tout |
| 2 | Idempotence par order_id au lieu de event_id | Double traitement si Wave envoie 2 events sur la même commande (init + completed) | Dédupliquer par event_id |
| 3 | Réponse 500 sur événement déjà traité | Wave retry 5x, log spam, alerting cassé | Retourner 200 OK même sur dedup |
| 4 | Lecture du body parsé par middleware | Signature invalide (body modifié) | Lire req.text() brut avant parse |
| 5 | Pas de timeout DB | Webhook bloque > 10s, Wave timeout, retry inutile | Index DB + transaction courte |
FAQ
Q : Combien de temps Wave attend une réponse ?
R : 10 secondes. Au-delà, Wave considère l'événement comme échoué et retry.
Q : Le webhook Wave envoie-t-il des doublons ?
R : Oui, en cas de retry suite à timeout. C'est exactement pourquoi l'idempotence est obligatoire.
Q : Faut-il une queue (Redis/BullMQ) ?
R : Pour < 1000 paiements/jour, un endpoint synchrone avec transaction Postgres suffit. Au-delà, déchargez vers une queue.
Notre intégration type chez Kolonell
Pour un client e-commerce moyen (boutique mode 200 commandes/mois), l'intégration Wave production-ready prend 47 minutes :
- 12 min : créer les routes API + table payment_events
- 15 min : implémenter signature HMAC + idempotence
- 10 min : tester avec fixture + commande réelle
- 10 min : connecter au workflow commande (email confirmation, MAJ statut)
Voir notre tutoriel Orange Money complémentaire →
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.
