Encaissez Orange Money et Wave avec une seule API, sans gérer les spécificités de chaque provider. Ce guide explique le comment avec des exemples concrets ; la référence exhaustive des champs reste le Swagger. URL de base : https://api.xmoney.xamle.io · JSON · montants en francs CFA entiers (devise XOF) · authentification Authorization: Bearer <clé API>.
| Concept | Rôle |
|---|---|
| Projet | Espace qui regroupe des transactions (une boutique, une app…). Tout paiement appartient à un projet. |
| Clé API | Secret xm_live_… qui authentifie votre serveur. Peut être limitée à un projet et à des scopes. |
| Paiement | Une demande d'argent : le payeur la règle via une page de paiement hébergée par X Money. |
| Payout | Un envoi d'argent vers un numéro Orange Money / Wave. |
| Webhook | Notification HTTP signée envoyée à votre serveur quand un événement survient. Optionnel. |
Cycle de vie d'un paiement : pending → succeeded | failed | expired (→ refunded après remboursement). Un paiement expire 5 minutes après sa création.
Dashboard → Développeurs → + Créer une clé :
read (lecture), payments (créer/gérer des encaissements), payouts (créer/gérer des décaissements).curl https://api.xmoney.xamle.io/v1/projects \ -H "Authorization: Bearer xm_live_..."
Toutes les erreurs ont le même format :
{ "error": { "code": "invalid-api-key", "message": "Clé API inconnue ou révoquée.", "doc_url": "…" } }checkout_url ;success_url / error_url si fournies.curl -X POST https://api.xmoney.xamle.io/v1/payments \
-H "Authorization: Bearer xm_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: commande-4521" \
-d '{
"provider": "auto",
"amount": 25000,
"currency": "XOF",
"project_id": "prj_...",
"merchant_reference": "CMD-4521",
"customer": { "name": "Awa Diop", "phone": "+221771234567" },
"success_url": "https://votre-site.sn/merci",
"error_url": "https://votre-site.sn/echec",
"metadata": { "commande": 4521 }
}'{
"id": "pay_01M2...",
"status": "pending",
"checkout_url": "https://api.xmoney.xamle.io/p/pay_01M2...",
"amount": 25000,
"expires_at": "2026-09-16T10:05:00.000Z"
}| Champ | Description |
|---|---|
provider | "auto" (recommandé : le payeur choisit sur la page), "orange_money" ou "wave" (imposé, initiation immédiate) |
amount | Entier ≥ 100. Omettez-le pour un montant libre : le payeur saisit lui-même le montant sur la page |
customer | Facultatif. Si absent, la page de paiement demande au payeur son nom et son numéro avant de payer |
merchant_reference | Votre référence interne (facture, commande) — visible dans le dashboard et les webhooks |
success_url / error_url | Redirection du payeur après verdict (?ref=<id> ou ?reason=<code>&ref=<id> ajoutés) |
metadata | Objet JSON libre, restitué tel quel |
Idempotency-Key unique par opération métier. En cas de retry réseau, la même clé renvoie le paiement déjà créé au lieu d'en créer un second.# Lecture simple curl https://api.xmoney.xamle.io/v1/payments/pay_01M2... \ -H "Authorization: Bearer xm_live_..." # Re-vérification active auprès du provider (si un payeur dit avoir payé) curl -X POST https://api.xmoney.xamle.io/v1/payments/pay_01M2.../sync \ -H "Authorization: Bearer xm_live_..."
status === "succeeded" côté serveur (webhook ou GET) — une redirection peut être interrompue ou forgée.| Endpoint | Effet |
|---|---|
GET /v1/payments?status=…&project_id=… | Liste paginée (starting_after pour la page suivante) |
POST /v1/payments/:id/expire | Expirer manuellement un paiement en attente |
POST /v1/payments/:id/refund | Rembourser un paiement Wave réussi |
Optionnel mais recommandé en production. Sans webhook, vous pouvez interroger GET /v1/payments/:id — la plateforme suit de toute façon chaque paiement. Le webhook évite simplement le polling : X Money pousse l'événement vers votre serveur dès qu'il survient.
Dashboard → Développeurs → Webhooks → ajoutez l'URL HTTPS de votre serveur. Le secret de signature est affiché une seule fois : stockez-le comme la clé API. Événements : payment.succeeded, payment.failed, payment.expired, refund.succeeded, payout.succeeded, payout.failed, payout.reversed, payout_batch.completed, settlement.created, settlement.completed.
Un POST sur votre URL, avec les headers X-Money-Event et X-Money-Signature: t=<timestamp>,v1=<hmac> :
{
"id": "evt_01M2...",
"type": "payment.succeeded",
"created_at": "2026-09-16T10:03:12.000Z",
"data": {
"payment": {
"id": "pay_01M2...",
"project_id": "prj_...",
"provider": "wave",
"amount": 25000,
"currency": "XOF",
"status": "succeeded",
"merchant_reference": "CMD-4521",
"metadata": { "commande": 4521 },
"settled_at": "2026-09-16T10:03:11.000Z"
}
}
}La signature est un HMAC-SHA256 du corps brut préfixé du timestamp : v1 = hexa( HMAC_SHA256( secret, timestamp + corpsBrut ) ). Rejetez si le timestamp a plus de 5 minutes (anti-rejeu).
const crypto = require('crypto');
app.post('/webhooks/xmoney', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.header('X-Money-Signature') ?? '';
const t = (header.match(/t=(\d+)/) ?? [])[1];
const v1s = [...header.matchAll(/v1=([a-f0-9]+)/g)].map((m) => m[1]);
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', process.env.XMONEY_WEBHOOK_SECRET)
.update(`${t}${req.body}`).digest('hex');
if (!v1s.some((v) => v.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected)))) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
// Traitez l'événement de façon IDEMPOTENTE (dédoublonnez sur event.id) :
// les retries peuvent livrer deux fois le même événement.
if (event.type === 'payment.succeeded') {
// ex. marquer la commande event.data.payment.merchant_reference comme payée
}
res.sendStatus(200); // répondez 2xx en < 10 s ; traitez le lourd en asynchrone
});id de l'événement).Nécessite le scope payouts. L'argent part de votre solde encaissé.
curl -X POST https://api.xmoney.xamle.io/v1/payouts \
-H "Authorization: Bearer xm_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: salaire-2026-09-fatou" \
-d '{
"provider": "wave",
"amount": 150000,
"currency": "XOF",
"project_id": "prj_...",
"recipient": { "mobile": "+221771234567", "name": "Fatou Ndiaye" },
"payment_reason": "Salaire septembre",
"merchant_reference": "SAL-2026-09-018"
}'recipient.mobile au format E.164 (+221…) ;payment_reason : 40 caractères max (affiché sur le reçu Wave) ;pending_approval → processing → succeeded | failed (| reversed après annulation) ;pending_approval et doit être approuvé dans le dashboard par un second utilisateur — votre intégration doit traiter ce statut comme normal ;Envoi en masse : POST /v1/payout-batches avec jusqu'à 500 payouts ; suivez l'ensemble via GET /v1/payout-batches/:id et l'événement payout_batch.completed. Annulation : POST /v1/payouts/:id/reverse (Wave uniquement, fenêtre de 3 jours).
Pour intégrer le checkout dans une application mobile, utilisez le SDK officiel plutôt qu'une WebView maison : il présente la page de paiement, ouvre les applications Wave / Orange Money / Max It via deeplink, suit le statut en temps réel et renvoie un résultat typé. Repo privé : github.com/Xamle-tech/xmoney-mobile-sdk (accès sur demande) — un seul repo, trois façons de consommer.
| Votre app | SDK | Installation |
|---|---|---|
| Flutter (Android + iOS) | x_money_flutter | dépendance pub par git, chemin flutter/x_money_flutter |
| Android natif (Kotlin/Java) | android/xmoney-sdk | module Gradle à inclure dans votre projet |
| iOS natif (Swift) | XMoneySDK | Swift Package Manager — ajoutez l'URL du repo dans Xcode |
Il n'existe pas de clé « publishable » : une app mobile distribuée au public ne doit jamais embarquer de clé xm_live_…. Le flux est donc :
POST /api/commander) ;POST /v1/payments, section 3) et renvoie checkout_url à l'app ;succeeded, failed (avec le code reason), cancelled ou dismissed (écran fermé) ;payment.succeeded.Quand le paiement est destiné au mobile, passez ces deux URLs sentinelles lors de la création (elles n'ont pas besoin d'exister : le SDK intercepte la navigation avant tout chargement) :
{
"success_url": "https://xmoney-sdk/return/success",
"error_url": "https://xmoney-sdk/return/error"
}Côté app, déclarez les schemes des providers pour que les deeplinks s'ouvrent — Android 11+ (manifest) et iOS (Info.plist) :
<queries>
<intent><action android:name="android.intent.action.VIEW" />
<data android:scheme="wave" /></intent>
<intent><action android:name="android.intent.action.VIEW" />
<data android:scheme="maxit" /></intent>
</queries><key>LSApplicationQueriesSchemes</key> <array> <string>wave</string> <string>maxit</string> </array>
import 'package:x_money_flutter/x_money_flutter.dart';
// checkoutUrl = renvoyé par votre backend
final result = await XMoneyCheckout.start(checkoutUrl: checkoutUrl);
switch (result.outcome) {
case XMoneyCheckoutOutcome.succeeded:
// UX de succès — la livraison réelle attend le webhook backend
case XMoneyCheckoutOutcome.failed:
// result.reason : 'expired', 'insufficient-funds', 'payer-cancelled'…
case XMoneyCheckoutOutcome.cancelled:
// annulé par le payeur
case XMoneyCheckoutOutcome.dismissed:
// écran fermé — le paiement peut encore aboutir (webhook)
}En natif, même contrat : XMoneyCheckoutContract (Android, registerForActivityResult) et XMoneyCheckout.present(from:…) (iOS) — exemples complets dans les README du repo. Un paiement expire au bout de 5 minutes (reason = "expired").
GET /v1/payments/:id) côté serveur.Créez le paiement avec un provider explicite ("wave" ou "orange_money") : la réponse contient wave_launch_url, deeplinks et qr_code. Construisez votre propre UI, ouvrez le deeplink, puis attendez le verdict avec le client de statut public du SDK (XMoneyStatusClient.pollUntilTerminal(checkoutUrl) — aucun secret requis, cadence 2,5 s).
Le SDK inclut aussi un client de l'API authentifiée (XMoneyApi en Dart, XMoneyApiClient en Kotlin/Swift) : payouts, soldes des deux poches, paiements.
const api = XMoneyApi(baseUrl: 'https://api.xmoney.xamle.io', apiKey: cleInterne); final payout = await api.createPayout( provider: 'wave', amount: 50000, recipient: const XMoneyPayoutRecipient(mobile: '+221771234567', name: 'Fatou Ndiaye'), projectId: 'prj_...', idempotencyKey: 'sal-2026-09-001', // OBLIGATOIRE — rejouez LA MÊME clé après un timeout ); // payout.status : succeeded, pendingApproval (4 yeux → dashboard), // processing (re-vérifier via api.syncPayout) ou failed (payout.errorCode)
Les règles de la section 5 s'appliquent telles quelles : pending_approval se débloque uniquement dans le dashboard, et les refus 422 (solde, plafonds) ne doivent pas être retentés.
| Endpoint | Description |
|---|---|
GET /v1/projects | Vos projets (une clé limitée à un projet ne voit que lui) |
GET /v1/balances?project_id=… | Soldes par provider (encaissé − commissions − décaissé) |
GET /v1/settlements | Historique des reversements vers votre compte |
GET /v1/reports/summary?from=…&to=… | Agrégats (volumes, taux de réussite) |
payouts) ;succeeded vérifié côté serveur ;Guide côté back-office : guide utilisateur.