Documentation de l'API Monea
Acceptez le Mobile Money auprès de nos partenaires de paiement agréés. Trois appels suffisent : vous créez une session, vous y envoyez votre client, vous recevez un webhook signé.
Introduction
Toutes les requêtes se font en HTTPS vers https://moneaa.com. Un domaine définitif remplacera cette adresse au passage en production.
Trois conventions à connaître
- Les montants sont des chaînes de caractères, pas des nombres :
"25000". Le franc CFA n'a pas de sous-unité, et une décimale non nulle est refusée plutôt que tronquée. - Les opérateurs ne sont pas une liste figée. Ils se lisent sur
GET /api/catalog, qui expose les pays, opérateurs et devises réellement disponibles. Elle dépend des partenaires de paiement avec lesquels Monea a contracté. N'en codez aucun en dur : la liste évolue sans préavis. - Le nombre de décimales dépend du couple opérateur + devise, jamais de la devise seule. Le champ
decimalsvautNONEouTWO_PLACES. CDF, TZS et UGX ont les deux selon l'opérateur. - Les numéros sont en chiffres uniquement, indicatif pays inclus et sans zéro initial :
22951345789.
Monea applique un modèle transparent où vous encaissez 100 % de votre prix net : les frais sont réglés par le client lors du paiement, et le partenaire de paiement agréé vous verse l'intégralité du prix demandé. Monea ne détient jamais vos fonds. Chaque transaction et webhook porte le détail dans son champ fee.
Authentification
Votre dashboard vous délivre quatre clés : une paire pour le sandbox, une pour la production. Les clés de production n'apparaissent qu'après validation de votre dossier (KYC ou KYB).
| Clé | Usage |
|---|---|
| pk_sandbox_fLx_… pk_live_fLx_… | Clé publique. Exposable côté client. |
| sk_sandbox_fLx_… sk_live_fLx_… | Clé secrète. Depuis votre serveur uniquement. |
Authorization: Bearer sk_sandbox_fLx_LtLK7JiIZyjcyiY6CIPdJHIjUne clé secrète n'est affichée qu'une seule fois, à sa génération : nous n'en conservons qu'une empreinte, pas la valeur. Si vous la perdez, régénérez-la — l'ancienne est invalidée immédiatement.
Calculer les frais
Pour afficher un total à votre client avant qu'il ne paie, sans rien engager. Ne recopiez jamais notre pourcentage dans votre code : il devient faux au premier changement de grille, et vous afficheriez un total que le paiement refuserait.
Mobile Money
Monea oriente chaque paiement vers le partenaire de paiement adapté à l'opérateur et au pays. Le total payé par votre client se calcule ainsi (conditions générales, article 9.3) : au prix s'ajoutent les frais Monea de 0,75 % + 50 FCFA, puis le total est calculé pour que les frais du partenaire, prélevés sur ce total, vous laissent 100 % du prix. Le montant est arrondi au franc supérieur. Dans l'exemple ci-dessous, le partenaire prélève 2 %.
POST /api/pricing/quote HTTP/1.1
Host: moneaa.com
Authorization: Bearer sk_live_fLx_...
Content-Type: application/json
{
"amount": "10000",
"provider": "MTN_MOMO_BEN",
"currency": "XOF",
"payment_method": "MOBILE_MONEY"
}{
"type": "collection",
"montant": "10000",
"frais": "332",
"total": "10332",
"recu": "10000",
"taux": 0.0332
}Vous recevez recu en entier : nos frais sont ajoutés au montant et payés par votre client.
Affichez toujours les trois lignes à votre client — montant, frais, total — et jamais le total seul.
Créer une session
Depuis votre serveur, avec votre clé secrète. Seul amount est obligatoire ; reference est votre identifiant de commande, et customerName alimente la colonne « Client » de votre dashboard — nous ne recevons aucun nom de l'opérateur.
POST /api/checkout/session HTTP/1.1
Host: moneaa.com
Authorization: Bearer sk_sandbox_fLx_...
Content-Type: application/json
{
"amount": "25000",
"reference": "CMD-4820",
"customerName": "Chabi Adéyèmi",
"description": "Commande #4820"
}{
"sessionId": "cb9a2435-d924-4f40-8832-441841ba3c7f",
"checkoutUrl": "/checkout?session=cb9a2435-d924-4f40-8832-441841ba3c7f",
"expiresAt": "2026-08-08T23:31:48.518Z"
}Redirigez ensuite votre client vers checkoutUrl. La session expire au bout de 30 minutes et ne peut déclencher qu'un seul paiement.
Encaisser
Notre page de paiement fait déjà ce travail. Ces deux appels ne vous concernent que si vous préférez construire votre propre écran de paiement — ils sont publics et n'exigent aucune clé, la session servant de jeton d'accès.
Lire la session à afficher
{
"amount": "25000",
"currency": "XOF",
"merchantName": "Ako Boutique",
"reference": "CMD-4820",
"description": "Commande #4820",
"limits": {
"MTN_MOMO_BEN": { "minAmount": "1", "maxAmount": "2000000" },
"MOOV_BEN": { "minAmount": "100", "maxAmount": "2000000" }
}
}Les bornes de limits viennent de l'opérateur et diffèrent d'un réseau à l'autre — 1 XOF minimum sur MTN, 100 sur Moov. Lisez-les plutôt que de les coder en dur.
Déclencher la demande de paiement
POST /api/checkout/pay HTTP/1.1
Content-Type: application/json
{
"sessionId": "cb9a2435-d924-4f40-8832-441841ba3c7f",
"provider": "MTN_MOMO_BEN",
"phoneNumber": "22951345789"
}{
"depositId": "09625597-958b-4569-907b-5b0f01dca685"
}Le client reçoit alors une demande de confirmation sur son téléphone et valide par son code secret. Il voit s'afficher MONEA 09625597 — le préfixe Monea suivi des huit premiers caractères du depositId, utile en cas de réclamation.
Suivre le statut
Pour l'écran d'attente. Interrogez toutes les trois secondes, le temps que le client valide.
{
"status": "pending",
"failureMessage": null
}| Statut | Signification |
|---|---|
| pending | En cours. Ne livrez rien. |
| completed | Payé. Le partenaire de paiement vous verse les fonds. |
| failed | Échoué. Aucun montant débité ; le motif est dans failureMessage. |
Un paiement peut rester en attente plusieurs minutes
Certaines transactions passent par une phase de réconciliation chez l'opérateur, qui n'a alors pas encore tranché. Nous ne les déclarons jamais échouées d'office. Traitez pending comme « pas encore payé », jamais comme un échec — et fiez-vous au webhook plutôt qu'à un délai.
Webhooks
Configurez votre URL dans le dashboard, une fois pour toutes — elle ne se passe pas par paiement. Elle doit être publique et en HTTPS. Répondez 200 pour confirmer : sans quoi nous réessayons cinq fois, en espaçant progressivement de 30 secondes à 4 minutes — soit environ huit minutes pour revenir en ligne. Passé ce délai, renvoyez l'événement depuis votre dashboard.
X-Monea-Signature: t=1786230167,v1=ac467b4b6d40da941b7d4ac0fe316258c9f40b71b1bdb9a95fb7c0b2ec782fe7
X-Monea-Event: deposit.completed
X-Monea-Delivery: c8605320-14ee-442c-8bdf-11b0c2097d18
Content-Type: application/json
User-Agent: Monea-Webhooks/1.0{
"event": "deposit.completed",
"createdAt": "2026-08-08T23:02:47.335Z",
"data": {
"depositId": "0ff96694-4a03-45b5-8f30-5dd7d5115d51",
"reference": "CMD-4820",
"amount": "25000",
"currency": "XOF",
"fee": "753",
"provider": "MTN_MOMO_BEN",
"payerPhone": "22951345789",
"customerName": "Chabi Adéyèmi",
"status": "completed",
"failureCode": null,
"failureMessage": null
}
}Vérifier la signature
À faire systématiquement. Sans cette vérification, n'importe qui connaissant votre URL pourrait vous annoncer un paiement réussi. L'horodatage est inclus dans la valeur signée, ce qui rend un rejeu détectable — refusez au-delà de cinq minutes.
import { createHmac, timingSafeEqual } from 'node:crypto'
// Le corps BRUT, avant tout parsing JSON : une réécriture de l'objet
// changerait l'ordre des clés et invaliderait la signature.
export function verifierWebhook(header, corpsBrut, secret) {
const parts = new Map(
header.split(',').map((p) => {
const [cle, ...reste] = p.split('=')
return [cle.trim(), reste.join('=').trim()]
}),
)
const horodatage = Number(parts.get('t'))
const fourni = parts.get('v1')
if (!Number.isFinite(horodatage) || !fourni) return false
// Au-delà de 5 minutes, on considère qu'il s'agit d'un rejeu.
if (Math.abs(Math.floor(Date.now() / 1000) - horodatage) > 300) return false
const attendu = createHmac('sha256', secret)
.update(`${horodatage}.${corpsBrut}`, 'utf8')
.digest('hex')
if (attendu.length !== fourni.length) return false
return timingSafeEqual(Buffer.from(attendu, 'hex'), Buffer.from(fourni, 'hex'))
}Événements émis
- deposit.completed
- deposit.failed
- payout.completed
- payout.failed
- kyb.approved
- kyb.rejected
Seuls les états définitifs déclenchent un envoi — aucun webhook pour un paiement encore en cours. Un même événement n'est envoyé qu'une fois, même si l'opérateur nous notifie en double.
Erreurs et échecs
Une requête refusée renvoie un code HTTP et un message directement affichable — il dit ce qui est attendu, pas seulement ce qui a échoué.
{
"message": "Montant hors limites pour ce moyen de paiement : 100 a 2000000 XOF",
"error": "Bad Request",
"statusCode": 400
}| Code | Cause |
|---|---|
| 400 | Montant hors limites, numéro invalide, session expirée ou déjà payée |
| 401 | Clé API absente ou invalide |
| 403 | Clé de production utilisée avant validation de votre dossier (KYC ou KYB) |
| 404 | Session ou paiement introuvable |
| 429 | Limite de débit atteinte — voir ci-dessous |
| 503 | Opérateur momentanément injoignable. Ne rejouez pas un paiement : consultez son statut |
Motifs d'échec d'un paiement
Transmis dans failureCode sur les webhooks deposit.failed.
| PAYER_NOT_FOUND | Le numéro n’a pas de compte Mobile Money actif |
| PAYMENT_NOT_APPROVED | Le payeur n’a pas validé sur son téléphone |
| INSUFFICIENT_BALANCE | Solde insuffisant sur le compte du payeur |
| PAYER_LIMIT_REACHED | Plafond de l’opérateur atteint par le payeur |
| WALLET_LIMIT_REACHED | Plafond du portefeuille atteint |
| PAYMENT_IN_PROGRESS | Un autre paiement est déjà en cours sur ce numéro |
| UNSPECIFIED_FAILURE | Échec sans cause précisée par l’opérateur |
| MONEA_TIMEOUT | Aucune confirmation dans le délai imparti — aucun débit |
Limites
Au-delà, l'API renvoie 429. Ces seuils couvrent largement un usage normal ; ils se remarquent surtout en test rapide.
| Opération | Requêtes | Fenêtre |
|---|---|---|
| Créer une session, encaisser | 10 | 1 minute |
| Lire une session | 60 | 1 minute |
| Suivre un statut | 120 | 1 minute |
| Demander un versement | 5 | 1 minute |
| Par défaut | 120 | 1 minute |
Les montants sont plafonnés par Monea (2 000 000 FCFA par paiement et par jour et 7 000 000 FCFA par mois pour un particulier ; 9 999 999 FCFA par paiement pour une entreprise) et par l'opérateur : lisez limits sur la session plutôt que de figer des valeurs.
Tester en sandbox
Vos clés sk_sandbox_ n'engagent aucun mouvement réel : une session ouverte avec une clé sandbox n'est jamais payée en argent réel, et le paiement est refusé si aucun partenaire de test ne sert le moyen choisi. Deux numéros pour commencer : 22951345789 réussit sur MTN, 22995345789 sur Moov. Demandez-nous le jeu complet pour éprouver chaque motif d'échec.
