Pour les développeurs

Documentation de l'API

Encaissez par Mobile Money (Orange Money, Moov Money, MTN MoMo, Wave, Free Money, Airtel Money, M-Pesa, Afrimoney et QMoney) au Burkina Faso, au Sénégal, en Côte d'Ivoire, au Bénin, au Cameroun, au Congo-Brazzaville, en RD Congo et en Gambie, sur votre site ou votre application. Vous créez un paiement depuis votre serveur, vous envoyez votre client sur notre page de paiement, et nous vous prévenons dès que c'est payé.

Démarrage rapide

  1. Créez un compte, puis copiez votre clé de test dans l'onglet « Développeurs ».
  2. Depuis votre serveur, créez un paiement avec POST /payments.
  3. Redirigez votre client vers checkout_url. Il paie, puis revient sur votre success_url.
  4. Confirmez le paiement (notification ou GET /payments/:id), puis livrez la commande.

Tous les montants sont des entiers sans décimales, exprimés dans la devise indiquée : monnaie du compte pour le paiement, monnaie du réseau pour le débit du client. Toutes les requêtes et réponses sont en JSON. URL de base :

https://yiriwapay.com/api/v1

Authentification

Envoyez votre clé secrète dans l'en-tête Authorization. Les clés sk_test_… n'utilisent aucun argent réel ; les clés sk_live_… encaissent de l'argent réel.

Authorization: Bearer sk_test_VOTRE_CLE

Ne mettez jamais votre clé dans une application mobile ou une page web : appelez toujours l'API depuis votre serveur.

Créer un paiement

POST /payments
curl -X POST https://yiriwapay.com/api/v1/payments \
  -H "Authorization: Bearer sk_test_VOTRE_CLE" \
  -H "Idempotency-Key: commande-1001" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000,
    "reference": "CMD-1001",
    "description": "Commande n°1001",
    "customer": { "name": "Awa Traoré", "phone": "70 12 34 56" },
    "success_url": "https://votre-site.com/merci",
    "cancel_url": "https://votre-site.com/panier",
    "metadata": { "order_id": "1001" }
  }'
ChampDescription
amount *Montant en F CFA, entier, de 200 à 1 000 000.
reference *Le numéro de votre commande (100 caractères max). Il vous sert à retrouver le paiement.
descriptionTexte affiché au client sur la page de paiement.
customerFacultatif : name, email, phone (numéro local, avec ou sans indicatif).
success_url / cancel_urlOù renvoyer le client après paiement / abandon. Nous ajoutons payment_id et reference à l'adresse.
metadataJusqu'à 20 paires clé / valeur, renvoyées dans les notifications.
fee_payermerchant (frais déduits de votre net) ou customer (frais ajoutés au prix). Par défaut : votre réglage.
expires_in_minutesValidité du lien : 5 à 1440 minutes (30 par défaut).
Réponse (201)
{
  "id": "6f1c2a3e-8b52-4d0e-9a5f-0d3c7e9b1a42",
  "object": "payment",
  "reference": "CMD-1001",
  "status": "created",
  "amount": 5000,
  "charged_amount": 5000,
  "fee_amount": 200,
  "net_amount": 4800,
  "currency": "XOF",
  "charged_currency": "XOF",
  "fee_payer": "merchant",
  "livemode": false,
  "checkout_url": "https://yiriwapay.com/pay/6f1c2a3e-…",
  "expires_at": "2026-10-12T10:30:00.000Z",
  "metadata": { "order_id": "1001" }
}

Redirigez votre client vers checkout_url : c'est la méthode recommandée (window.location.href = checkout_url). La page est en français, adaptée aux téléphones, et aux couleurs de votre entreprise. Les frais indiqués dans la réponse sont une estimation (ici 4 %, le réseau le plus cher) : le frais définitif est celui du réseau choisi par le client. currency reste la devise du paiement et charged_currency indique la devise débitée sur le réseau choisi. XOF et XAF gardent leur parité fixe 1:1 ; pour CDF, GMD et les autres paires, un taux direct doit être activé par l'administration. Un taux inverse n'est pas déduit automatiquement.

Vous pouvez aussi l'ouvrir dans une fenêtre ou dans un cadre : <iframe src="…checkout_url…" style="width:100%;height:640px;border:0"></iframe>. Attention : les paiements Wave envoient le client sur la page de Wave, qui ne peut pas s'afficher dans un cadre. Si vous acceptez Wave, utilisez la redirection.

Important : le retour du client sur votre success_url ne prouve pas qu'il a payé (n'importe qui peut ouvrir cette adresse). Avant de livrer, confirmez toujours le paiement par une notification ou en lisant le paiement avec l'API.

Exemples de code

Le même appel, dans trois langages. Mettez votre clé dans une variable d'environnement de votre serveur, jamais dans le code que vous publiez.

Node.js
const res = await fetch("https://yiriwapay.com/api/v1/payments", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.YIRIWAPAY_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": "commande-1001",
  },
  body: JSON.stringify({ amount: 5000, reference: "CMD-1001", success_url: "https://votre-site.com/merci" }),
});
const payment = await res.json();
if (!res.ok) throw new Error(payment.error.message);
// Redirigez votre client vers payment.checkout_url
PHP
<?php
$ch = curl_init('https://yiriwapay.com/api/v1/payments');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . getenv('YIRIWAPAY_KEY'),
    'Content-Type: application/json',
    'Idempotency-Key: commande-1001',
  ],
  CURLOPT_POSTFIELDS => json_encode(['amount' => 5000, 'reference' => 'CMD-1001', 'success_url' => 'https://votre-site.com/merci']),
]);
$payment = json_decode(curl_exec($ch), true);
if (!isset($payment['checkout_url'])) { die($payment['error']['message'] ?? 'Erreur'); }
header('Location: ' . $payment['checkout_url']);
Python
import os, requests

r = requests.post(
    "https://yiriwapay.com/api/v1/payments",
    headers={"Authorization": "Bearer " + os.environ["YIRIWAPAY_KEY"], "Idempotency-Key": "commande-1001"},
    json={"amount": 5000, "reference": "CMD-1001", "success_url": "https://votre-site.com/merci"},
    timeout=30,
)
r.raise_for_status()
checkout_url = r.json()["checkout_url"]  # redirigez votre client ici

Statuts

createdCréé, le client n'a pas encore payé.
pendingLe client a lancé le paiement, l'opérateur le traite.
succeededPayé. Vous pouvez livrer.
failedÉchec définitif (5 essais refusés).
expiredLe délai est dépassé. Rarement, un paiement expiré peut passer à « succeeded » si l'opérateur confirme tardivement : traitez toujours les notifications.
cancelledAnnulé par vous avant tout paiement.

Un client qui se trompe (mauvais code, solde insuffisant) peut réessayer jusqu'à 5 fois sur le même lien : le paiement reste « created » entre deux essais.

Vérifier un paiement

GET /payments/:id
curl https://yiriwapay.com/api/v1/payments/6f1c2a3e-… \
  -H "Authorization: Bearer sk_test_VOTRE_CLE"

C'est la source de vérité. Vérifiez que status vaut succeeded et que amount et reference correspondent à votre commande.

Notifications (webhooks)

Enregistrez l'adresse d'une page de votre serveur dans l'onglet « Développeurs ». À chaque événement, nous envoyons un POST JSON signé :

Exemple d'événement
{
  "id": "evt_5c2e0b7a4d1f4c0e9d2a6b8f1e3a7c90",
  "type": "payment.succeeded",
  "created": 1791800000,
  "livemode": false,
  "data": { "object": { "id": "6f1c2a3e-…", "object": "payment", "status": "succeeded", "reference": "CMD-1001", "amount": 5000, "net_amount": 4800 } }
}

Événements : payment.succeeded, payment.failed, payment.expired, payment.cancelled, payout.succeeded, payout.failed. Répondez par un code 2xx dès que vous avez reçu l'événement. Sinon nous réessayons après 1 min, 5 min, 30 min, 2 h, 12 h puis 24 h. Vous pouvez recevoir le même événement plusieurs fois : ignorez les doublons grâce à son id.

Vérifiez la signature (en-tête YiriwaPay-Signature, de la forme t=…,v1=…) avec le corps brut de la requête et votre secret whsec_… :

Node.js
import crypto from "node:crypto";

export function verifyYiriwaPay(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // trop ancien : rejeu possible
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const received = parts.v1 ?? "";
  return expected.length === received.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
PHP
<?php
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_YIRIWAPAY_SIGNATURE'] ?? ''), $p);
$ok = isset($p['t'], $p['v1'])
  && abs(time() - (int)$p['t']) <= 300
  && hash_equals(hash_hmac('sha256', $p['t'] . '.' . $raw, $secret), $p['v1']);
if (!$ok) { http_response_code(400); exit; }
Python
import hashlib, hmac, time

def verify_yiriwapay(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", 0))
    if not t or abs(time.time() - t) > 300:  # trop ancien : rejeu possible
        return False
    expected = hmac.new(secret.encode(), str(t).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Annuler, lister, solde

POST https://yiriwapay.com/api/v1/payments/:id/cancel      # annule un paiement jamais commencé
GET  https://yiriwapay.com/api/v1/payments?limit=20&status=succeeded&reference=CMD-1001&starting_after=:id
GET  https://yiriwapay.com/api/v1/balance                  # { "available": 48250, "pending": 9650, "currency": "XOF" }

La liste renvoie { "object": "list", "data": [ …paiements… ], "has_more": true }. Tant que has_more est vrai, redemandez la page suivante avec starting_after = l'id du dernier paiement reçu.

Retraits et remboursements

Les retraits se demandent depuis le tableau de bord (Portefeuille) : il n'existe pas de point d'accès d'API pour les créer. Si vous avez enregistré une adresse de notification, vous recevez payout.succeeded quand un retrait est envoyé et payout.failed quand il est refusé (l'argent est alors remis dans votre solde).

Les remboursements de clients ne sont pas pris en charge par l'API pour l'instant : vous restez responsable de rembourser vos clients. Si un client a payé deux fois la même commande, nous le détectons et le remboursons. Pour un cas particulier, contactez-nous.

Idempotence

Si votre appel échoue à cause du réseau, vous ne savez pas si le paiement a été créé. Ajoutez l'en-tête Idempotency-Key avec une valeur unique par commande (par exemple commande-1001) : en cas de nouvel essai, nous renvoyons le même paiement au lieu d'en créer un second.

Limites de requêtes

Pour protéger tout le monde, l'API accepte environ 100 requêtes par minute et par clé. Créer un paiement (POST /payments) et annuler un paiement ont leur propre compteur : une boucle qui lit vos paiements ne bloque jamais vos clients qui paient. La limite est approximative : elle sert à arrêter les abus, pas à mesurer votre consommation.

Chaque réponse indique où vous en êtes :

X-RateLimit-LimitNombre de requêtes permises par minute.
X-RateLimit-RemainingRequêtes encore permises dans la minute en cours.
X-RateLimit-ResetFin de la minute en cours (heure Unix, en secondes).
Retry-AfterSeulement avec une erreur 429 : le nombre de secondes à attendre avant de réessayer.

Au-delà de la limite, vous recevez 429 rate_limited : aucune action n'a été faite, vous pouvez donc rejouer la même requête après l'attente (gardez la même Idempotency-Key). Envoyer beaucoup de clés invalides depuis la même adresse (plus de 20 par minute) bloque aussi cette adresse pendant une minute.

Réessayer proprement (Node.js)
async function callYiriwaPay(url, options, tries = 3) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url, options);
    if (res.status !== 429) return res;
    const wait = Number(res.headers.get("Retry-After") ?? 1);
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  throw new Error("Trop de requêtes : réessayez plus tard.");
}

Erreurs

Format
HTTP 422
{ "error": { "code": "invalid_request", "message": "Le montant minimum est de 200 F" } }
401 invalid_api_keyClé absente, invalide ou révoquée.
403 forbidden / live_not_enabledAction interdite, compte suspendu ou mode live pas encore activé.
404 not_foundPaiement introuvable.
409 conflictÉtat incompatible (ex. annuler un paiement déjà commencé) ou clé d'idempotence réutilisée avec d'autres paramètres.
422 invalid_requestDonnées invalides : le message indique le champ.
429 rate_limitedTrop de requêtes. Attendez Retry-After secondes puis réessayez : rien n'a été fait.
500 internal_errorErreur de notre côté : réessayez avec la même Idempotency-Key.

Mode test

Avec une clé sk_test_…, aucun argent réel n'est utilisé. Sur la page de paiement :

  • Orange Money : le code OTP 123456 réussit, tout autre code échoue.
  • Un numéro finissant par 00 échoue (« solde insuffisant »).
  • Un numéro finissant par 99 reste « en attente » : des boutons « Simuler un succès / un échec » apparaissent.
  • Tout autre numéro réussit tout de suite.

Les notifications du mode test sont envoyées comme en production (adresse de test à enregistrer séparément).

Limites et frais

  • Montant : de 200 unités (minimum imposé par le prestataire de paiement, non modifiable) à 1 000 000 unités dans la devise du paiement, frais compris si le client les paie.
  • Pays et réseaux : Burkina Faso (Orange Money, Moov Money), Sénégal (Orange Money, Free Money, Wave), Côte d'Ivoire (MTN MoMo, Moov Money, Orange Money, Wave), Bénin (Moov Money), Cameroun (Orange Money, MTN MoMo), Congo-Brazzaville (MTN MoMo, Airtel Money), RD Congo (Airtel Money, M-Pesa, Orange Money), Gambie (Afrimoney, QMoney). Le client choisit son pays et son réseau sur la page de paiement. Le paiement reste dans currency (F CFA (XOF), F CFA d'Afrique centrale (XAF) pour le Cameroun et le Congo-Brazzaville, franc congolais (CDF) pour la RD Congo, dalasi (GMD) pour la Gambie) ; il est débité dans la devise du réseau si un taux direct actif est disponible.
  • charged_amount et fee_amount sont exprimés en charged_currency. amount et net_amount restent dans currency. Les taux hors parité CFA sont fixés par l'administration ; aucune conversion n'est disponible sans taux direct actif.
  • Le frais dépend du réseau choisi par le client : à la création, fee_amount et charged_amount sont une estimation (le réseau le plus cher), puis ils sont ajustés quand le client choisit son réseau. Les valeurs définitives sont celles du paiement réussi.
  • Les frais ne s'appliquent qu'aux paiements réussis. Votre grille tarifaire est visible dans « Paramètres ».
  • Les fonds encaissés deviennent retirables après le délai de règlement de votre compte.

Outils et assistants IA

Importez notre spécification OpenAPI dans Postman, Insomnia ou un générateur de code : elle décrit tous les points d'accès, les champs et les erreurs.

Pour un assistant IA (ChatGPT, Claude, Cursor…), donnez-lui l'adresse /llms.txt : un résumé de l'API avec les liens vers cette documentation.