Documentation API

Tout ce qu'il faut pour encaisser par Mobile Money et par carte depuis votre back-end : authentification, création de paiements et d'abonnements, checkout hébergé, webhooks.

Vue d'ensemble

L'API myapp-pay est organisée en REST, accepte et renvoie du JSON. Toutes les requêtes se font en HTTPS vers :

https://www.myapp-pay.com/api/v1

Chaque application (une par produit/projet) a ses propres clés API et son propre historique, séparés en deux environnements sandbox et live — voir Sandbox / Live. Les montants (amount) sont des entiers exprimés dans l'unité de base de la devise (ex. XOF, qui n'a pas de décimales) — jamais de centimes.

Authentification

Chaque requête est authentifiée via un en-tête Authorization: Bearer portant une clé API, générée depuis le tableau de bord (Application → Clés API). Deux types de clés, au format {pk|sk}_{test|live}_... :

  • sk_... (secrète) — requise pour créer un paiement ou un abonnement. À garder côté serveur uniquement, jamais exposée dans du code client.
  • pk_... (publique) — suffit pour consulter un paiement ou un abonnement existant.

Le segment test/live de la clé correspond à l'environnement sandbox/live affiché dans le tableau de bord — une clé sk_test_... n'agit que sur les données sandbox de l'application, une sk_live_... que sur les données live.

Authorization: Bearer sk_test_XXXXXXXXXXXXXXXXXXXXXXXX

Sandbox / Live

Le sandbox est utilisable immédiatement après inscription, sans validation préalable — de quoi intégrer et tester avant toute revue manuelle. Aucun provider réel n'est appelé en sandbox : les paiements et décaissements y sont simulés, aucune vraie somme ne bouge.

Le mode live nécessite que votre compte et votre application soient approuvés en back-office (contrôle anti-abus, pas une formalité automatique) — tant que ce n'est pas le cas, toute requête en live renvoie 403 account_not_approved ou application_not_approved.

Idempotence

POST /payments et POST /subscriptions exigent un en-tête Idempotency-Key (une chaîne unique de votre choix, un UUID par exemple). Il protège contre les doubles créations en cas de retry réseau :

  • Première requête avec cette clé → création, réponse 201.
  • Rejeu avec la même clé et les mêmes paramètres (montant, méthode…) → aucune nouvelle création, la ressource existante est renvoyée avec 200.
  • Rejeu avec la même clé mais des paramètres différents → 409 idempotency_key_conflict (la clé doit être unique par tentative logique, pas réutilisée pour autre chose).
  • En-tête absent → 400 missing_idempotency_key.

Une clé d'idempotence n'est unique que par application (deux applications peuvent réutiliser la même clé sans conflit).

Méthodes & pays actifs

GET/api/v1/payment-methods

method et country sont des identifiants libres — aucune liste fixe n'existe côté myapp-pay, ils dépendent des providers actifs sur votre compte. Cet endpoint renvoie exactement les combinaisons qui fonctionneront sur POST /payments pour l'environnement de la clé utilisée (même logique de résolution qu'à la création — un couple absent d'ici renverra toujours no_provider_available).

curl https://www.myapp-pay.com/api/v1/payment-methods \
  -H "Authorization: Bearer pk_test_XXXXXXXXXXXXXXXXXXXXXXXX"
{
  "card": true,
  "countries": {
    "CI": ["mtn_ci", "moov_ci"],
    "TG": ["tmoney_tg", "moov_tg"]
  }
}

card: true signifie qu'au moins un provider actif accepte la carte — sans distinction de pays (voir plus bas). countries ne couvre que le Mobile Money : la clé est le pays, la valeur la liste des méthodes utilisables pour ce pays précis. En sandbox, ceci reflète la configuration du provider de test, pas un vrai réseau Mobile Money.

Cohérence method/country : pour le Mobile Money, le country envoyé doit être celui sous lequel cette méthode apparaît dans countries ci-dessus (typiquement le pays de l'opérateur — ex. mtn_ci avec country: "CI"). Un couple incohérent (ex. mtn_ci avec country: "SN") n'est jamais deviné ni corrigé silencieusement : ça renvoie 400 no_provider_available.

Portée de card : le paiement carte n'est pas résolu par pays — country reste obligatoire dans la requête (traçabilité) mais n'a aucune influence sur l'éligibilité. Un client de n'importe quel pays peut donc payer par carte dès que card: true ci-dessus. Les marques de carte acceptées et d'éventuelles restrictions par pays émetteur dépendent de votre provider carte (PayDunya) — non documentées côté myapp-pay, à vérifier directement auprès d'eux si besoin.

Bonne pratique recommandée : les frais carte sont généralement plus élevés que le Mobile Money. Pour un pays présent dans countries ci-dessus, proposez d'abord ses méthodes Mobile Money à votre client ; réservez card aux pays absents de countries (non couverts par le Mobile Money) ou aux clients qui n'ont explicitement pas d'autre moyen. Ce n'est qu'une recommandation — myapp-pay ne bloque jamais la carte pour un pays où le Mobile Money existe : c'est à vous, qui connaissez vos clients, de décider quoi leur proposer.

Créer un paiement

POST/api/v1/payments

Crée un paiement en attente et son URL de checkout. N'appelle aucun provider tout de suite : ça n'arrive que lorsque le client valide son moyen de paiement sur la page de checkout.

ChampTypeRequisDescription
amountintegerouiMontant entier, dans l'unité de base de currency (ex. FCFA, sans décimales).
methodstringouiIdentifiant libre de méthode (ex. "mtn_ci", "orange_money_sn", "card") — dépend des providers actifs sur votre compte pour ce pays.
countrystringouiCode pays ISO 3166-1 alpha-2 (ex. "CI").
currencystringouiCode devise ISO 4217 (ex. "XOF").
descriptionstringnonLibellé affiché sur le checkout, 500 caractères max.
customer.emailstringnon—
customer.phonestringnonEnregistré pour votre suivi client ; le client re-saisit son numéro sur le checkout, ce champ ne déclenche pas de requête Mobile Money.
customer.fullNamestringnon—
returnUrlstringnonURL (http/https) où renvoyer le client une fois le paiement résolu sur le checkout — voir plus bas.

Exemple de requête :

curl https://www.myapp-pay.com/api/v1/payments \
  -X POST \
  -H "Authorization: Bearer sk_test_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f29a1c2-9e77-4b34-9b21-79c2a6e2b9f1" \
  -d '{
    "amount": 5000,
    "method": "mtn_ci",
    "country": "CI",
    "currency": "XOF",
    "description": "Abonnement Pro - Septembre",
    "customer": { "phone": "+2250700000000", "email": "client@example.com" },
    "returnUrl": "https://votre-site.com/merci"
  }'

Réponse 201 (ou 200 si la clé d'idempotence rejoue une création précédente) :

{
  "id": "cmf3x9k8j0001v0abcd12efgh",
  "status": "pending",
  "method": "mtn_ci",
  "amount": 5000,
  "currency": "XOF",
  "checkoutUrl": "https://www.myapp-pay.com/checkout/cmf3x9k8j0001v0abcd12efgh",
  "description": "Abonnement Pro - Septembre",
  "customerReference": null,
  "createdAt": "2026-09-04T12:00:00.000Z",
  "paidAt": null
}

Redirigez le client vers checkoutUrl pour qu'il finalise le paiement — voir Le checkout hébergé. customerReference est toujours null pour un paiement créé via cette API (réservé aux liens de paiement créés depuis le tableau de bord).

Consulter un paiement

GET/api/v1/payments/:id

Clé publique ou secrète acceptée. Renvoie le même objet que la création, avec le statut à jour.

curl https://www.myapp-pay.com/api/v1/payments/cmf3x9k8j0001v0abcd12efgh \
  -H "Authorization: Bearer pk_test_XXXXXXXXXXXXXXXXXXXXXXXX"

404 transaction_not_found si l'identifiant n'existe pas, appartient à une autre application, ou à l'autre environnement (sandbox/live) que celui de la clé utilisée.

Le checkout hébergé

checkoutUrl pointe vers une page hébergée par myapp-pay, sans authentification requise (l'identifiant de transaction, imprévisible, fait office de jeton d'accès). Le client y saisit son numéro Mobile Money (ou les informations de carte) et confirme — c'est à ce moment que le provider est réellement sollicité, jamais avant.

L'issue du paiement arrive de façon asynchrone : suivez-la via un webhook (recommandé) ou en interrogeant GET /payments/:id.

Si vous avez fourni returnUrl à la création, le client y est automatiquement renvoyé (~2 secondes après résolution, avec un lien de secours affiché au cas où) une fois le paiement réussi ou échoué — avec transactionId et status ajoutés en query params. Ce statut est indicatif seulement (visible et modifiable par le client dans son navigateur) : ne délivrez jamais rien sur cette seule base, confirmez toujours via le webhook ou GET /payments/:id avant. Sans returnUrl, le client reste sur cette page après résolution — pensez à le fournir si votre client a un site/appli où revenir. Aucune inscription préalable du domaine n'est requise : n'importe quelle URL http:///https:// est acceptée, revalidée à chaque affichage du checkout (jamais javascript:, qui serait rejetée).

La méthode ne se choisit jamais sur cette page : method est figé à la création du paiement (il détermine le provider et les frais dès ce moment-là), le client ne peut que saisir les informations de paiement correspondantes. Si votre site doit proposer un sélecteur pays/méthode, c'est à vous de le construire en amont (avec la liste des méthodes actives) et de créer le paiement avec le choix du client — myapp-pay ne propose pas encore ce sélecteur sur son propre checkout hébergé.

Tester en sandbox

En sandbox, aucun provider réel n'est appelé : le checkout confirme lui-même le paiement via un webhook auto-signé, en success par défaut. Pour tester votre gestion des échecs (webhook payment.failed, page de retour en échec…), un bouton « Simuler un échec » apparaît sous le bouton de paiement sur le checkout — visible uniquement en sandbox, jamais en live.

Créer un abonnement

POST/api/v1/subscriptions

Crée un abonnement et pousse immédiatement la demande de paiement de la première période — pas de checkout à visiter, le téléphone et la méthode sont fournis directement ici (comme pour chaque relance à venir).

ChampTypeRequisDescription
planIdstringouiIdentifiant d'un plan actif de l'application, créé depuis le tableau de bord.
methodstringouiIdentifiant libre de méthode Mobile Money.
customer.phonestringoui6 à 20 caractères.
customer.emailstringnon—
customer.fullNamestringnon—
customer.countrystringnonCode pays ISO 3166-1 alpha-2.

Exemple de requête :

curl https://www.myapp-pay.com/api/v1/subscriptions \
  -X POST \
  -H "Authorization: Bearer sk_test_XXXXXXXXXXXXXXXXXXXXXXXX" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9c1e2f3a-0b4d-4e5a-9f11-1a2b3c4d5e6f" \
  -d '{
    "planId": "plan_abc123",
    "method": "mtn_ci",
    "customer": { "phone": "+2250700000000", "email": "client@example.com" }
  }'

Réponse 201 :

{
  "id": "sub_xyz789",
  "status": "active",
  "planId": "plan_abc123",
  "method": "mtn_ci",
  "currentPeriodEnd": "2026-10-04T12:00:00.000Z"
}

404 plan_not_found si planId n'existe pas, n'appartient pas à votre application, n'est pas dans l'environnement de la clé utilisée, ou est désactivé.

Consulter un abonnement

GET/api/v1/subscriptions/:id
curl https://www.myapp-pay.com/api/v1/subscriptions/sub_xyz789 \
  -H "Authorization: Bearer pk_test_XXXXXXXXXXXXXXXXXXXXXXXX"
{
  "id": "sub_xyz789",
  "status": "active",
  "planId": "plan_abc123",
  "method": "mtn_ci",
  "currentPeriodEnd": "2026-10-04T12:00:00.000Z",
  "retryCount": 0
}

Codes d'erreur

Toute erreur renvoie un corps de la forme suivante, avec un statut HTTP approprié :

{ "error": { "code": "transaction_not_found", "message": "Transaction introuvable" } }
Statut HTTPcodeQuand
400invalid_requestCorps de requête invalide (champ manquant ou mal typé) — voir details dans la réponse.
400missing_idempotency_keyEn-tête Idempotency-Key absent sur un POST qui le requiert.
400invalid_moneyMontant invalide (ex. négatif).
400fee_computation_failedÉchec interne du calcul de frais.
400provider_fee_table_errorAucune règle de frais pour cette méthode chez le provider résolu.
400no_provider_availableAucun provider actif ne couvre ce pays/méthode.
400ambiguous_provider_routingPlusieurs règles de routage concurrentes pour ce pays/méthode (configuration back-office à corriger).
401authentication_failedEn-tête Authorization manquant, clé invalide, révoquée, ou clé publique utilisée là où une clé secrète est requise.
403account_not_approvedCompte refusé/suspendu, ou pas encore approuvé pour le live.
403application_not_approvedApplication refusée/suspendue, ou pas encore approuvée pour le live.
404transaction_not_foundPaiement introuvable pour cette application/environnement.
404subscription_not_foundAbonnement introuvable pour cette application/environnement.
404plan_not_foundPlan introuvable, inactif, ou dans l'autre environnement.
409idempotency_key_conflictClé d'idempotence déjà utilisée avec des paramètres différents.
409transaction_not_payableLe paiement n'est plus dans un état permettant de le faire progresser.
409subscription_not_renewableL'abonnement n'est plus dans un état permettant une relance.
501provider_not_implementedOpération non supportée par le provider résolu.
503provider_not_configuredLe provider résolu n'a pas (encore) d'identifiants configurés en back-office.

Webhooks

Configurez un endpoint depuis Application → Webhooks. À la création, un secret vous est montré une seule fois — conservez-le, il sert à vérifier la signature de chaque livraison.

Chaque livraison est un POST avec ces en-têtes :

ChampTypeRequisDescription
X-Myapp-Eventstring—Nom de l'événement, ex. payment.succeeded.
X-Myapp-Signaturestring—Format t=<timestamp unix>,v1=<HMAC-SHA256 hex>.

Vérification (Node.js) :

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const timestamp = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Important : calculez le HMAC sur le corps brut de la requête (avant tout parsing JSON), sinon la signature ne correspondra jamais.

Événements émis :

ÉvénementDéclenché quand
payment.succeededLe paiement est confirmé par le provider.
payment.failedLe paiement échoue ou est annulé côté provider.
subscription.renewedUne période d'abonnement est facturée avec succès.
subscription.payment_failedUne relance échoue mais l'abonnement a encore des tentatives.
subscription.suspendedL'abonnement est suspendu après trop d'échecs consécutifs.

Note : le formulaire du tableau de bord ne propose aujourd'hui de cocher que payment.succeeded/payment.failed à la création d'un endpoint ; le support des événements subscription.* dans cette interface arrive prochainement — contactez-nous si vous en avez besoin dès maintenant.

Exemple de payload (payment.succeeded) :

{
  "id": "cmf3x9k8j0001v0abcd12efgh",
  "status": "success",
  "method": "mtn_ci",
  "amountGross": "5000",
  "currency": "XOF",
  "amountNet": "4750",
  "paidAt": "2026-09-04T12:03:21.000Z",
  "failureReason": null,
  "customerReference": null
}

amountGross correspond toujours au amount envoyé/renvoyé par POST /payments et GET /payments/:id (nom différent uniquement ici, où amountNet est affiché à côté pour comparaison). Les deux sont des chaînes dans le webhook (BigInt sérialisé), un nombre dans le reste de l'API. amountNet = amountGross moins les frais provider et la commission plateforme — c'est ce montant, pas amountGross, qui est effectivement crédité à votre solde retirable.

Réessais : jusqu'à 6 tentatives par livraison en cas de non-succès (délai croissant : 30s, 1min, 2min, 4min, 8min, jusqu'à 16min), puis la livraison est marquée épuisée. Consultez l'historique des livraisons (statuts, payloads, dernière erreur) depuis Application → Webhooks.

Référence des statuts

Paiement (status)

StatutSignification
pendingCréé, en attente d'action du client sur le checkout ou de confirmation provider.
successPayé.
failedÉchoué (voir failureReason côté webhook).
expiredRéservé pour un usage futur — rien ne fixe ce statut aujourd'hui, voir note ci-dessous.
cancelledAnnulé.

Aucune expiration automatique aujourd'hui : un paiement jamais finalisé par le client reste indéfiniment en pending, sans bascule vers expired après un délai. En revanche, si le client tente réellement de payer et que le provider rejette la charge (numéro invalide, solde insuffisant à l'initiation…), la transaction passe désormais en failed avec un failureReason explicite et un webhook payment.failed — elle ne reste plus bloquée en pending dans ce cas (corrigé début septembre 2026, suite à un ticket support).

Abonnement (status)

StatutSignification
activeÀ jour.
past_dueDernière relance échouée, nouvelles tentatives à venir.
suspendedTrop d'échecs consécutifs — plus de relance automatique.
canceledRésilié.

Limites & bonnes pratiques

Limite de débit (rate limiting) : aucune limite n'est aujourd'hui appliquée sur /api/v1/* (création comme consultation) — dit autrement, un usage abusif ne renverra pas d'erreur 429 pour l'instant. Ne vous reposez pas sur cette absence de limite : elle est susceptible d'évoluer sans préavis, gardez vos intégrations raisonnables (pas de boucle de polling agressive sur GET /payments/:id, préférez le webhook).

Durée de vie d'une Idempotency-Key : conservée indéfiniment, aucune expiration automatique. Une clé déjà utilisée pour un paiement ou un abonnement de votre application reste associée à cette création pour toujours — générez-en une nouvelle à chaque tentative logiquement distincte plutôt que d'en réutiliser une ancienne en espérant une nouvelle création.

Une question, un cas non couvert ici ? Contactez-nous ou retrouvez vos clés et webhooks depuis votre tableau de bord.

Contact

Une question qui n'est pas dans la FAQ ?

Écris-nous, on te répond directement.