Aller au contenu
Monea — L'agrégateur de paiements
Retour à l'accueil
Sandbox

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 decimals vaut NONE ou TWO_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.
bash
Authorization: Bearer sk_sandbox_fLx_LtLK7JiIZyjcyiY6CIPdJHIj

Une 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 %.

requête Mobile Money
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"
}
réponse · 200
{
  "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.

requête
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"
}
réponse · 201
{
  "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

GET /api/checkout/session/:sessionId
{
  "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

requête
POST /api/checkout/pay HTTP/1.1
Content-Type: application/json

{
  "sessionId": "cb9a2435-d924-4f40-8832-441841ba3c7f",
  "provider": "MTN_MOMO_BEN",
  "phoneNumber": "22951345789"
}
réponse · 200
{
  "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.

GET /api/checkout/status/:depositId
{
  "status": "pending",
  "failureMessage": null
}
StatutSignification
pendingEn cours. Ne livrez rien.
completedPayé. 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.

en-têtes reçus
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
corps
{
  "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.

vérification
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é.

réponse · 400
{
  "message": "Montant hors limites pour ce moyen de paiement : 100 a 2000000 XOF",
  "error": "Bad Request",
  "statusCode": 400
}
CodeCause
400Montant hors limites, numéro invalide, session expirée ou déjà payée
401Clé API absente ou invalide
403Clé de production utilisée avant validation de votre dossier (KYC ou KYB)
404Session ou paiement introuvable
429Limite de débit atteinte — voir ci-dessous
503Opé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_FOUNDLe numéro n’a pas de compte Mobile Money actif
PAYMENT_NOT_APPROVEDLe payeur n’a pas validé sur son téléphone
INSUFFICIENT_BALANCESolde insuffisant sur le compte du payeur
PAYER_LIMIT_REACHEDPlafond de l’opérateur atteint par le payeur
WALLET_LIMIT_REACHEDPlafond du portefeuille atteint
PAYMENT_IN_PROGRESSUn autre paiement est déjà en cours sur ce numéro
UNSPECIFIED_FAILUREÉchec sans cause précisée par l’opérateur
MONEA_TIMEOUTAucune 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érationRequêtesFenêtre
Créer une session, encaisser101 minute
Lire une session601 minute
Suivre un statut1201 minute
Demander un versement51 minute
Par défaut1201 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.