Wave Business API en production repose sur l’architecture la plus simple à intégrer du marché africain : une API REST cohérente, des préfixes de clé qui identifient l’environnement d’un coup d’œil, et une logique de checkout hébergé qui isole votre backend des contraintes PCI. La transition du sandbox vers la production reste néanmoins un moment à préparer : KYC business, IP whitelisting unilatéral, rotation des clés et signature HMAC sur les webhooks doivent être en place avant le premier paiement réel.
Ce guide reprend la mise en production de bout en bout pour la Wave Business API, en supposant que vous avez déjà fait fonctionner un appel Checkout en sandbox et que vous disposez d’un compte Wave Business actif. Le code d’exemple utilise Node.js mais les concepts (signature HMAC sur body brut, refresh anticipé du token, idempotence côté webhook) sont identiques en Python, PHP, Go ou Ruby.
Prérequis pour Wave Business API en production
- Un compte Wave Business activé avec accès au Wave Business Portal
- Un prototype Checkout ou Payout fonctionnel en sandbox
- Une URL HTTPS publique stable pour les webhooks (jamais ngrok en production)
- Des IPs sortantes connues pour tous vos serveurs : web, worker, cron, jobs de réconciliation
- Un gestionnaire de secrets (Vault, AWS Secrets Manager, Doppler ou équivalent)
Checklist E2E production-ready (à valider avant la première transaction réelle)
Le passage en production ne se résume pas à changer une clé. Avant la première transaction live, sept éléments doivent être verts ; un seul manquant et la rupture surviendra au pire moment (typiquement au cinquième jour, quand les premiers retries de webhooks de Wave commencent à arriver). Cette check-list à cocher dans l’ordre :
- KYC validé — onglet Developer activé dans le Wave Business Portal, mention Live visible.
- Clé production stockée dans un secret manager — Vault, AWS Secrets Manager, Doppler ; jamais dans
.envversionné, ni dans une variable Docker en clair. - IPs sortantes whitelistées — toutes les machines (web, workers, cron, jobs réconciliation, CI staging si appelants) en
/32, vérifiées par uncurl ifconfig.medepuis chacune. - Sandbox parity — flux nominal testé à l’identique en sandbox avant bascule : création session, redirection client, callback succès, réception webhook, marquage transaction.
- Vérification HMAC obligatoire et raw body préservé — handler webhook avec
express.raw()en amont, comparaison entimingSafeEqual, rejet si timestamp > 5 min, retour 401 sur signature invalide. - Idempotence — contrainte unique sur
event_idWave en base,INSERT … ON CONFLICT DO NOTHINGsur la table de log webhooks, plusclient_referenceindexé sur la table transactions. - Monitoring + alerting — métriques Prometheus ou équivalent : taux 401/403/429 sur appels sortants, taux de signatures webhook invalides, latence p95, échec de réconciliation J-1. Page-out sur seuils stricts.
La règle pratique : aucune communication marketing du nouveau moyen de paiement avant que ces sept points soient verts depuis au moins 48 heures de production sandbox-like (trafic réel mais montants faibles, type test interne).
Étape 1 — Cartographier les différences sandbox vs production
Wave maintient l’hôte api.wave.com pour les deux environnements. La discrimination se fait par le préfixe de la clé d’API. Une clé sandbox commence par wave_sn_test_, une clé production par wave_sn_prod_. Cette convention est précieuse : un grep dans votre code suffit à vérifier qu’aucune clé test n’a fuité vers la prod et inversement. Documentez cette règle dans vos pre-commit hooks (recherche du pattern wave_sn_(test|prod)_[a-z0-9] dans le code source pour bloquer un commit accidentel).
Le contenu des réponses diffère en production : les transactions sont réelles, les soldes évoluent, les webhooks arrivent depuis l’infrastructure prod de Wave. Le format JSON reste identique. Côté quotas, le sandbox tolère un trafic illimité non débité, la production applique des limites de débit (rate limits) en fonction du plan business négocié avec Wave. Demandez à votre account manager les limites exactes : elles déterminent la stratégie de retry et de batching côté backend.
Étape 2 — Compléter le KYC business sur le portail Wave
L’onboarding production démarre dans le Wave Business Portal sous Settings → Business Profile. Le formulaire exige le nom légal de l’entité, le numéro d’enregistrement (RC, NINEA, identifiant fiscal selon le pays), l’adresse du siège, le secteur d’activité, l’estimation du volume mensuel attendu, le nom du représentant légal et son numéro de pièce d’identité. Joignez les pièces justificatives en PDF : statuts à jour, attestation fiscale, RIB du compte bancaire de versement.
Le délai d’instruction varie d’une semaine à trois selon les pays et la complétude du dossier. Une fois validé, votre compte Wave Business passe en mode « Live » et l’onglet Developer du portail devient pleinement actif : génération des clés production, gestion des webhooks, IP whitelisting.
Étape 3 — Générer et stocker la clé production
Dans la section Developer → API Keys, cliquez sur Create production key. Donnez un nom explicite à la clé (par exemple backend-prod-fr-2026-q2) qui identifie le contexte d’usage et l’année de rotation prévue. Cochez les permissions strictement nécessaires : pour un usage Checkout pur, seules les permissions Sessions create, Sessions read et Webhook receive suffisent. N’octroyez jamais Transfer create à une clé qui ne sert qu’à l’encaissement.
La clé s’affiche une seule fois en clair sous la forme wave_sn_prod_ suivie d’une longue chaîne. Copiez-la immédiatement vers votre gestionnaire de secrets. Si vous la perdez, vous devrez la révoquer et en créer une nouvelle.
# Stockage immédiat dans HashiCorp Vault
vault kv put secret/wave/prod \
api_key="wave_sn_prod_..." \
webhook_secret="wave_sn_WHS_..."
Si vous utilisez AWS Secrets Manager, l’équivalent est aws secretsmanager create-secret --name wave/prod --secret-string. Quel que soit l’outil, jamais de clé en clair dans un fichier .env commit en repo, jamais dans une issue GitHub, jamais dans un Slack pro.
Étape 4 — Configurer l’IP whitelisting avec précaution
Le whitelisting d’IPs Wave est unilatéral : dès que vous ajoutez une première adresse, l’enforcement s’active automatiquement et il ne peut plus être désactivé depuis le portail. Cette particularité impose de préparer la liste exhaustive avant la première soumission.
Listez toutes les machines qui appelleront api.wave.com : serveurs web, workers asynchrones (Celery, Sidekiq, Bull), tâches cron, jobs de réconciliation, serveurs de staging si vous souhaitez tester depuis votre intégration continue. Pour chaque machine, notez l’IP publique sortante effective, pas l’IP privée du datacenter. Si vous êtes derrière un NAT ou un load balancer, c’est l’IP du NAT qui doit figurer. Wave recommande des plages /32 par serveur, ce qui est la pratique la plus sûre. Les plages plus larges (/24 par exemple) augmentent la surface d’attaque inutilement.
Une fois la liste prête, ajoutez les IPs une par une dans Developer → IP Whitelist. Vérifiez immédiatement avec un appel test depuis chaque serveur :
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer $WAVE_API_KEY" \
https://api.wave.com/v1/transactions?first=1
Un code 200 confirme l’accès, un 403 signale que l’IP source n’est pas dans la liste. Pour les pays où votre plage IP change (hébergement chez certains providers qui ne garantissent pas l’IP), envisagez de passer par un proxy sortant dédié avec IP fixe et de ne whitelister que cette IP.
Étape 5 — Créer une session Checkout en production
L’API Checkout suit un schéma cohérent. Votre backend appelle POST /v1/checkout/sessions avec le montant en unité minimale, la devise (XOF pour la zone UEMOA), les URLs de retour et l’identifiant de référence interne. Wave répond avec une wave_launch_url que vous redirigez vers le navigateur du client. Le client paie sur la page hébergée Wave et revient sur votre success_url.
const fetch = require('node-fetch');
const crypto = require('crypto');
async function createCheckoutSession(amount, reference) {
// L'anti-doublon repose sur client_reference (contrainte unique en base côté backend)
const res = await fetch('https://api.wave.com/v1/checkout/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WAVE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: String(amount),
currency: 'XOF',
error_url: `https://app.exemple.com/checkout/error?ref=${reference}`,
success_url: `https://app.exemple.com/checkout/success?ref=${reference}`,
client_reference: reference,
}),
});
if (!res.ok) throw new Error(`Wave ${res.status}: ${await res.text()}`);
return res.json();
}
Le header Idempotency-Key n’est pas formellement documenté sur l’endpoint Checkout sessions ; la protection anti-doublon repose alors sur la valeur client_reference que vous fournissez et que vous indexez en base avec une contrainte unique. En cas de retry sur timeout, vous interrogez d’abord l’API par client_reference avant toute nouvelle initiation. La client_reference est votre identifiant interne ; elle apparaît dans les rapports Wave et dans les webhooks ultérieurs, ce qui facilite la réconciliation.
Le même appel en Python (requests + retry exponentiel via urllib3.Retry) — version production-ready avec pool d’adapters HTTPS et timeouts explicites :
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def _wave_session():
s = requests.Session()
retry = Retry(
total=3, backoff_factor=2,
status_forcelist=[502, 503, 504],
allowed_methods=["POST", "GET"],
raise_on_status=False,
)
s.mount("https://", HTTPAdapter(max_retries=retry, pool_connections=10))
return s
def create_checkout(amount_xof: int, client_reference: str) -> dict:
s = _wave_session()
r = s.post(
"https://api.wave.com/v1/checkout/sessions",
headers={
"Authorization": f"Bearer {os.environ['WAVE_API_KEY']}",
"Content-Type": "application/json",
},
json={
"amount": str(amount_xof),
"currency": "XOF",
"success_url": "https://votresite.sn/wave/success",
"error_url": "https://votresite.sn/wave/error",
"client_reference": client_reference,
},
timeout=(5, 15), # connect, read
)
if r.status_code == 429:
raise RuntimeError("rate_limited") # déléguer au worker
r.raise_for_status()
return r.json()
Le tuple timeout=(5, 15) sépare l’établissement de connexion (5 s) du temps de lecture (15 s) : un proxy d’entreprise qui n’ouvre pas le canal échoue vite, mais un appel Wave qui répond lentement n’est pas tué à 5 secondes. Le raise_on_status=False du Retry évite que requests lève une exception sur un 429 qui doit remonter au worker pour rejouer plus tard plutôt que d’épuiser les retries du client HTTP.
Version PHP (Guzzle 7 + middleware retry) — utilisée typiquement côté WooCommerce ou Laravel :
<?php
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\RequestInterface;
function wave_client(): Client {
$stack = HandlerStack::create();
$stack->push(Middleware::retry(
fn(int $r, RequestInterface $req, ?ResponseInterface $res = null, $ex = null) =>
$r < 3 && ($ex || ($res && in_array($res->getStatusCode(), [502,503,504]))),
fn(int $r) => 1000 * (2 ** $r), // ms
));
return new Client([
'base_uri' => 'https://api.wave.com',
'handler' => $stack,
'timeout' => 15,
'connect_timeout' => 5,
'headers' => [
'Authorization' => 'Bearer ' . getenv('WAVE_API_KEY'),
'Content-Type' => 'application/json',
],
]);
}
function create_checkout(int $amountXof, string $clientRef): array {
$res = wave_client()->post('/v1/checkout/sessions', [
'json' => [
'amount' => (string) $amountXof,
'currency' => 'XOF',
'success_url' => 'https://votresite.sn/wave/success',
'error_url' => 'https://votresite.sn/wave/error',
'client_reference' => $clientRef,
],
]);
return json_decode((string) $res->getBody(), true);
}
Le middleware Guzzle gère le retry automatiquement sur 502/503/504 avec un backoff exponentiel (1 s, 2 s, 4 s). Les 4xx remontent immédiatement à l’appelant via une exception Guzzle — c’est intentionnel : une 401 ou une 403 sont des erreurs de configuration qu’aucun retry ne corrigera.
Version Go (net/http standard, sans dépendance externe) — utile pour un microservice de paiement isolé :
package wave
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type Session struct {
ID string `json:"id"`
WaveLaunchURL string `json:"wave_launch_url"`
Status string `json:"status"`
}
var httpClient = &http.Client{Timeout: 15 * time.Second}
func CreateCheckout(ctx context.Context, amountXof int64, clientRef string) (*Session, error) {
payload, _ := json.Marshal(map[string]string{
"amount": fmt.Sprintf("%d", amountXof),
"currency": "XOF",
"success_url": "https://votresite.sn/wave/success",
"error_url": "https://votresite.sn/wave/error",
"client_reference": clientRef,
})
req, _ := http.NewRequestWithContext(ctx, "POST",
"https://api.wave.com/v1/checkout/sessions", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("WAVE_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
return nil, fmt.Errorf("wave checkout failed: %d", res.StatusCode)
}
var s Session
if err := json.NewDecoder(res.Body).Decode(&s); err != nil {
return nil, err
}
return &s, nil
}
En Go, le pattern est volontairement minimal : context.Context pour la propagation du deadline depuis le handler HTTP appelant (annulation propre si l’utilisateur ferme l’onglet avant la redirection), un http.Client partagé en variable de package (réutilisation des connexions TCP), pas de retry interne — le retry sera porté par le worker asynchrone qui orchestre la création de session. À la sortie de la fonction, le champ WaveLaunchURL est l’URL à fournir au navigateur du client.
Étape 6 — Recevoir et vérifier les webhooks signés
Wave envoie un webhook signé à l’URL configurée dans Developer → Webhooks. Le header Wave-Signature contient un timestamp et un HMAC-SHA256 calculé sur la concaténation directe du timestamp et du body brut (sans séparateur), avec votre secret webhook comme clé. La vérification côté serveur est non négociable.
const express = require('express');
const crypto = require('crypto');
const app = express();
// IMPORTANT : conserver le body brut, ne pas le parser avant la vérification
app.post('/webhooks/wave', express.raw({type:'application/json'}), (req, res) => {
const signatureHeader = req.headers['wave-signature'];
if (!signatureHeader) return res.status(400).send('Missing signature');
// Parser : t=,v1=[,v1=...] — plusieurs v1 lors rotation de secret
let timestamp = null;
const signatures = [];
for (const part of signatureHeader.split(',')) {
const [k, v] = part.split('=', 2);
if (k === 't') timestamp = v;
else if (k === 'v1' && v) signatures.push(v);
}
if (signatures.length === 0) return res.status(400).send('Missing v1 signature');
const tsNum = Number(timestamp);
if (!Number.isFinite(tsNum)) return res.status(400).send('Bad timestamp');
const age = Math.abs(Date.now()/1000 - tsNum);
if (age > 300) return res.status(400).send('Stale webhook');
// HMAC sur le buffer brut, pas sur une conversion utf8 qui pourrait altérer
const hmac = crypto.createHmac('sha256', process.env.WAVE_WEBHOOK_SECRET);
hmac.update(String(timestamp));
hmac.update(req.body);
const computed = hmac.digest('hex');
const computedBuf = Buffer.from(computed, 'hex');
// Accepter si UNE des signatures matche (support rotation côté Wave)
let ok = false;
for (const expectedSig of signatures) {
if (!/^[0-9a-f]{64}$/.test(expectedSig)) continue;
const expectedBuf = Buffer.from(expectedSig, 'hex');
if (crypto.timingSafeEqual(computedBuf, expectedBuf)) { ok = true; break; }
}
if (!ok) return res.status(400).send('Bad signature');
// Idempotence : contrainte unique sur event_id en base, INSERT ... ON CONFLICT DO NOTHING
const event = JSON.parse(req.body.toString('utf8'));
// ... apply business effect via transaction unique
res.status(200).send('ok');
});
Trois pièges classiques sur cette vérification. Premier piège : un middleware express.json() positionné avant cette route consomme le body et casse la signature. Deuxième piège : crypto.createHmac doit recevoir la chaîne brute (timestamp directement concaténé au body, sans séparateur) et non pas le body parsé puis re-stringifié. Troisième piège : la comparaison doit utiliser timingSafeEqual sur des buffers de même longueur (sinon il lève une exception) ; valider le format hex avant tout. Quatrième piège : un header avec plusieurs v1= (rotation de secret) doit être parsé en liste — un simple Object.fromEntries ne garde que la dernière signature et casse la rotation.
L’idempotence côté webhook est tout aussi importante. Wave peut retenter un webhook plusieurs fois en cas de réponse non-200 sur une fenêtre allant jusqu’à trois jours. Indexez vos transactions par event_id Wave avec une contrainte unique en base, et faites un INSERT ... ON CONFLICT DO NOTHING dès la réception. Si la ligne existe déjà, vous renvoyez 200 immédiatement sans ré-appliquer l’effet métier.
La même vérification HMAC en Python (Flask, mais l’API hmac.compare_digest est identique en Django / FastAPI) :
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
WAVE_WEBHOOK_SECRET = os.environ["WAVE_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/wave")
def wave_webhook():
raw = request.get_data() # CRITIQUE : body brut, pas request.json !
header = request.headers.get("Wave-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
ts, v1 = parts.get("t"), parts.get("v1")
if not ts or not v1:
abort(401)
if abs(time.time() - int(ts)) > 300: # 5 minutes
abort(401)
expected = hmac.new(WAVE_WEBHOOK_SECRET, (ts + raw.decode()).encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, v1):
abort(401)
event = request.get_json() # OK ici, signature déjà validée
# idempotence : INSERT ON CONFLICT sur event["id"]
return ("", 200)
Le piège Flask classique : request.json ou request.get_json() appelés avant get_data() consomment le body et invalident la signature à coup sûr. La règle absolue : lire le raw une fois, vérifier la signature, et seulement ensuite parser le JSON pour le traitement métier.
Version PHP brute (sans framework — fonctionne identique dans un contrôleur Laravel ou Symfony si vous récupérez $request->getContent() ou file_get_contents("php://input")) :
<?php
$secret = getenv('WAVE_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // body brut, jamais $_POST
$header = $_SERVER['HTTP_WAVE_SIGNATURE'] ?? '';
parse_str(strtr($header, ',', '&'), $parts);
$ts = $parts['t'] ?? null;
$v1 = $parts['v1'] ?? null;
if (!$ts || !$v1 || abs(time() - (int)$ts) > 300) {
http_response_code(401);
exit;
}
$expected = hash_hmac('sha256', $ts . $raw, $secret);
if (!hash_equals($expected, $v1)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// INSERT ... ON CONFLICT DO NOTHING sur $event['id']
http_response_code(200);
Trois fonctions PHP critiques ici : file_get_contents("php://input") pour le raw (jamais $_POST qui décoderait l’url-encoded), hash_hmac pour le calcul HMAC, et hash_equals pour la comparaison en temps constant (équivalent du timingSafeEqual de Node.js). Une comparaison == ou === classique laisse fuir des bits via les attaques par timing.
Version Go (handler net/http, utilisable derrière une gateway ou en standalone) :
package wavewebhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func Handler(w http.ResponseWriter, r *http.Request) {
raw, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read body", http.StatusBadRequest)
return
}
header := r.Header.Get("Wave-Signature")
var ts, v1 string
for _, p := range strings.Split(header, ",") {
kv := strings.SplitN(p, "=", 2)
if len(kv) == 2 {
switch kv[0] {
case "t": ts = kv[1]
case "v1": v1 = kv[1]
}
}
}
if ts == "" || v1 == "" {
http.Error(w, "bad sig", http.StatusUnauthorized); return
}
tsInt, _ := strconv.ParseInt(ts, 10, 64)
if abs(time.Now().Unix()-tsInt) > 300 {
http.Error(w, "expired", http.StatusUnauthorized); return
}
mac := hmac.New(sha256.New, []byte(os.Getenv("WAVE_WEBHOOK_SECRET")))
mac.Write([]byte(ts))
mac.Write(raw)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(v1)) {
http.Error(w, "mismatch", http.StatusUnauthorized); return
}
// INSERT ... ON CONFLICT DO NOTHING sur event_id
w.WriteHeader(http.StatusOK)
}
func abs(x int64) int64 { if x < 0 { return -x }; return x }
Spécificité Go : on écrit le timestamp et le raw body en deux appels successifs à mac.Write() plutôt qu’en concaténant via une troisième allocation. Ce n’est pas un détail de perf — c’est aussi plus sûr car la chaîne ts+raw en string allouée pourrait être tracée en mémoire ou loguée par accident. hmac.Equal garantit la comparaison à temps constant, équivalent strict du timingSafeEqual Node.js.
Étape 7 — Mettre en place la réconciliation quotidienne
Aucune intégration paiement n’est complète sans réconciliation J+1. Le scénario à anticiper : votre application a marqué une commande payée via webhook, mais l’agrégat des transactions Wave de la veille n’inclut pas cette ligne (ou inversement, une transaction Wave n’a pas trouvé d’écho côté application). Sans contrôle quotidien automatisé, un écart de quelques transactions peut s’accumuler sur plusieurs semaines avant qu’un client réclame et déclenche une investigation pénible. La défense systémique : un job cron à 02h00 (avant l’arrivée du support) qui récupère toutes les transactions Wave de la veille, les confronte à votre base par client_reference, et écrit un rapport. Trois sorties possibles à scénariser : 100 % match (rien à faire), écart minoritaire (le rapport identifie nominativement les écarts et part au support), écart majoritaire (alerting page-out, suspendre les nouvelles transactions le temps de comprendre).
L’API Balance & Reconciliation de Wave permet d’exporter les transactions par journée avec pagination forward via curseur after. Un job nocturne télécharge la liste des transactions du jour précédent et la confronte à votre base interne. Toute transaction présente côté Wave mais absente côté SI est un signe d’incident webhook ; toute transaction présente côté SI mais absente côté Wave est plus rare et indique souvent un problème de propagation.
async function listTransactions(date, afterCursor) {
const url = new URL('https://api.wave.com/v1/transactions');
url.searchParams.set('date', date);
if (afterCursor) url.searchParams.set('after', afterCursor);
const res = await fetch(url, {
headers: {'Authorization': `Bearer ${process.env.WAVE_API_KEY}`}
});
return res.json(); // { date, items: [...], page_info: { has_next_page, end_cursor } }
}
Stockez les écarts détectés dans une table d’audit et envoyez une alerte si la quantité dépasse un seuil. Un écart isolé n’est pas un problème ; un écart récurrent au même créneau horaire pointe vers une coupure réseau régulière côté webhook qu’il faut investiguer.
Erreurs fréquentes
| Erreur | Cause probable | Solution |
|---|---|---|
| 401 Unauthorized | Clé révoquée ou mauvaise environnement | Vérifier le préfixe et l’état dans le portail |
| 403 Forbidden | IP source non whitelistée | Vérifier l’IP sortante effective via curl ifconfig.me |
| 429 Too Many Requests | Dépassement du rate limit | Retry exponentiel et batching côté backend |
| Webhook signature invalide | Body parsé avant vérification | Utiliser express.raw() en amont de la route |
| Double application d’un effet | Pas d’idempotence sur event_id | Contrainte unique en base et INSERT ON CONFLICT |
| Session expirée côté client | Durée de vie par défaut dépassée | Régénérer la session plutôt que recycler l’ancienne |
FAQ
Combien de clés API peut-on créer en production ? Wave ne limite pas le nombre. Créez une clé par service (backend, worker, batch), avec permissions minimales. Une clé compromise se révoque sans toucher aux autres.
L’IP whitelisting peut-il être désactivé ? Pas depuis le portail. Contactez votre account manager Wave si la désactivation est strictement nécessaire ; en pratique, la solution est de retirer toutes les IPs sauf une de « fallback ».
Quelle stratégie de retry sur 5xx ? Retry avec backoff exponentiel, base 2, plafond 60 secondes, maximum trois tentatives. Au-delà, marquez la transaction en échec et déclenchez l’investigation manuelle.
Le sandbox reflète-t-il fidèlement la production ? Pour le flux nominal, oui. Pour les rate limits et les latences réelles, non — le sandbox est plus permissif. Testez la résilience en injectant des erreurs simulées côté backend (chaos engineering léger).
Ressources officielles
- Documentation Wave Business — docs.wave.com/business
- Checkout API — docs.wave.com/checkout
- Payout API — docs.wave.com/payout
- Webhooks Wave — docs.wave.com/webhook
- RFC 2104 HMAC — datatracker.ietf.org/doc/html/rfc2104
Article de référence : Intégrer les APIs Mobile Money en production : Wave, Orange Money, MTN MoMo, Moov. Tutoriels frères : MTN MoMo sandbox vers production, Orange Money Web Payment en production.
À combiner avec : tutoriel d’intégration Orange Money e-commerce pour offrir un parcours d’achat sans friction.