WavePaiement Mobile MoneySpring BootWebhooksSécuritéFintech

Intégrer Wave : paiements mobile money côté backend avec Spring Boot

Publié le 13 août 2026

Résumé

Comment j'ai intégré Wave dans notre API Spring Boot : checkout sessions, signature HMAC des requêtes sortantes, vérification des webhooks avec protection anti-replay, et réconciliation automatique des paiements en attente

## Pourquoi Wave


Sur le marché sénégalais, le paiement se fait très majoritairement par mobile money. Pour une SaaS de gestion de patrimoine commercial destinée aux commerçants de Dakar, intégrer Wave

n'était pas une option secondaire — c'était l'un des moyens de paiement principals attendus par nos utilisateurs pour souscrire à un abonnement.


Cet article décrit l'intégration côté backend (Spring Boot / Java) : de l'ouverture d'une session de paiement jusqu'à l'activation de l'abonnement via webhook.


## Architecture : un PaymentProvider par méthode de paiement


Plutôt que de coder Wave en dur dans le service de paiement, on isole chaque intégration derrière une interface PaymentProvider. Le service central résout le bon provider selon la méthode

choisie :


`java

PaymentProvider provider = resolveProvider(PaymentMethod.WAVE);


Ce choix paie immédiatement : ajouter Orange Money ou un virement bancaire plus tard ne touche pas à l'orchestration existante, seulement à une nouvelle implémentation.

Initier un paiement


Le flux démarre sur POST /api/v1/payments/initiate. Le service crée un Payment en base avec le statut PENDING, puis délègue au provider Wave :


- construction du body : amount, currency=XOF, client_reference (l'ID interne du paiement), success_url / error_url

- signature du body brut

- appel POST https://api.wave.com/v1/checkout/sessions


Piège n°1 — le format du montant. L'API Wave attend un entier, pas une valeur décimale. En XOF il n'y a pas de sous-unité utilisable dans ce contexte, donc le montant est envoyé via

toBigInteger(). Envoyer un BigDecimal avec décimales fait échouer la requête silencieusement côté intégration si on n'y prête pas attention.


La réponse de Wave renvoie une wave_launch_url (à rediriger côté client) et un id de session, qu'on stocke sur le Payment en passant son statut à PROCESSING.


Signer les requêtes sortantes


Wave exige une signature HMAC sur les appels sortants, au format :


t=,v1=


calculée sur timestamp + rawBody, envoyée dans le header Wave-Signature. La même logique de signature/vérification sert dans les deux sens : pour signer nos requêtes vers Wave, et pour vérifier

les webhooks qu'on reçoit d'eux.


Recevoir la confirmation : le webhook

Le paiement n'est jamais confirmé côté client — uniquement via webhook, sur POST /api/v1/payments/webhooks/wave. Trois règles non négociables ici :


1. Vérifier la signature avant de parser le payload. Si le header Wave-Signature est absent ou invalide, on rejette la requête sans même lire le corps.

2. Protection anti-replay. La signature est rejetée si le timestamp diffère de plus de 5 minutes par rapport à l'heure serveur — ça bloque le rejeu d'un webhook intercepté.

3. Idempotence. Wave peut renvoyer le même événement plusieurs fois (retry réseau, etc.). On garde une trace du waveEventId déjà traité pour ignorer les doublons — sans ça, un abonnement

pourrait être activé/facturé deux fois.


Deux types d'événements sont gérés : checkout.session.completed (succès si payment_status=succeeded) et checkout.session.payment_failed. Le paiement retrouvé via client_reference (ou à défaut le

waveSessionId) passe à COMPLETED ou FAILED, puis déclenche l'activation ou l'upgrade de l'abonnement lié.


Et si le webhook n'arrive jamais ?


Les webhooks peuvent se perdre. On ne peut pas laisser un utilisateur avec un paiement PROCESSING indéfiniment. Deux filets de sécurité :


- un endpoint POST /verify qui interroge directement GET checkout/sessions/{id} pour vérifier l'état réel côté Wave

- un job planifié (2h du matin) qui reprend les paiements restés PROCESSING trop longtemps, les revérifie, et expire ceux qui dépassent une fenêtre de validité définie par configuration


Ce job gère aussi le renouvellement automatique des abonnements arrivant à échéance.


Ce que je retiens

- Isoler chaque provider de paiement derrière une interface commune évite que l'intégration d'un moyen de paiement contamine les autres.

- Ne jamais faire confiance à la confirmation côté client : le webhook signé est la seule source de vérité.

- L'idempotence sur les webhooks n'est pas optionnelle — un même événement va arriver plusieurs fois en production.

- Un job de réconciliation planifié est indispensable dès qu'on dépend d'un webhook externe pour faire avancer un état métier.


Prochaine étape : appliquer le même pattern PaymentProvider pour Orange Money.