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
- Créez un compte, puis copiez votre clé de test dans l'onglet « Développeurs ».
- Depuis votre serveur, créez un paiement avec
POST /payments. - Redirigez votre client vers
checkout_url. Il paie, puis revient sur votresuccess_url. - 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/v1Authentification
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_CLENe mettez jamais votre clé dans une application mobile ou une page web : appelez toujours l'API depuis votre serveur.
Créer un paiement
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" }
}'| Champ | Description |
|---|---|
| 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. |
| description | Texte affiché au client sur la page de paiement. |
| customer | Facultatif : name, email, phone (numéro local, avec ou sans indicatif). |
| success_url / cancel_url | Où renvoyer le client après paiement / abandon. Nous ajoutons payment_id et reference à l'adresse. |
| metadata | Jusqu'à 20 paires clé / valeur, renvoyées dans les notifications. |
| fee_payer | merchant (frais déduits de votre net) ou customer (frais ajoutés au prix). Par défaut : votre réglage. |
| expires_in_minutes | Validité du lien : 5 à 1440 minutes (30 par défaut). |
{
"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.
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.
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
$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']);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 iciStatuts
| created | Créé, le client n'a pas encore payé. |
| pending | Le client a lancé le paiement, l'opérateur le traite. |
| succeeded | Payé. Vous pouvez livrer. |
| failed | Échec définitif (5 essais refusés). |
| expired | Le délai est dépassé. Rarement, un paiement expiré peut passer à « succeeded » si l'opérateur confirme tardivement : traitez toujours les notifications. |
| cancelled | Annulé 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
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é :
{
"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_… :
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
$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; }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-Limit | Nombre de requêtes permises par minute. |
| X-RateLimit-Remaining | Requêtes encore permises dans la minute en cours. |
| X-RateLimit-Reset | Fin de la minute en cours (heure Unix, en secondes). |
| Retry-After | Seulement 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.
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
HTTP 422
{ "error": { "code": "invalid_request", "message": "Le montant minimum est de 200 F" } }| 401 invalid_api_key | Clé absente, invalide ou révoquée. |
| 403 forbidden / live_not_enabled | Action interdite, compte suspendu ou mode live pas encore activé. |
| 404 not_found | Paiement 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_request | Données invalides : le message indique le champ. |
| 429 rate_limited | Trop de requêtes. Attendez Retry-After secondes puis réessayez : rien n'a été fait. |
| 500 internal_error | Erreur 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
123456réussit, tout autre code échoue. - Un numéro finissant par
00échoue (« solde insuffisant »). - Un numéro finissant par
99reste « 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_amountetfee_amountsont exprimés encharged_currency.amountetnet_amountrestent danscurrency. 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_amountetcharged_amountsont 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.