## 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=
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.