Intégration directe (votre propre design)
Votre client choisit son moyen de paiement sur votre site, avec votre design. Votre serveur appelle l’API AIFI PAY ; le client valide MVola sur son téléphone.
Paiement hébergé ou intégration directe ?
C’est la même API, avec la même clé secrète, les mêmes notifications et la même commission. Seul l’appel de départ change.
- Paiement hébergé (
POST /v1/checkout/sessions) : vous redirigez le client vers la page AIFI PAY. Le plus simple, recommandé pour démarrer. - Intégration directe (
POST /v1/payments) : vous construisez votre page de paiement, vous collectez le numéro MVola du client et vous affichez vous-même l’attente et le résultat.
Vous pouvez utiliser les deux, par exemple le paiement hébergé sur votre site et l’intégration directe dans votre application.
Comment ça marche
- Votre serveur demande les moyens de paiement disponibles et votre page n’affiche que ceux-là.
- Le client choisit MVola et saisit son numéro sur votre page. Votre serveur crée le paiement avec ce numéro.
- Le client reçoit la demande sur son téléphone et la valide avec son code secret MVola. Pendant ce temps, votre page affiche votre propre écran d’attente.
- Votre serveur apprend le résultat par la notification signée (recommandé) ou en lisant le paiement. Vous validez alors la commande.
1. Lister les moyens de paiement disponibles
/v1/payment-methodsRenvoie les moyens de paiement de l’environnement de votre clé (Sandbox ou Production). N’affichez comme payables que ceux où available vaut true ; les autres peuvent apparaître comme « Bientôt ».
{
"environment": "PRODUCTION",
"methods": [
{ "id": "MVOLA", "name": "MVola", "available": true,
"flow": "phone_confirmation", "requiredFields": ["provider", "customer.phone"] },
{ "id": "ORANGE_MONEY", "name": "Orange Money", "available": false,
"flow": "redirect", "requiredFields": ["provider", "successUrl"] },
{ "id": "AIRTEL_MONEY", "name": "Airtel Money", "available": false,
"flow": "phone_confirmation", "requiredFields": [] }
]
}phone_confirmation: le client valide sur son téléphone. Envoyez son numéro danscustomer.phone.redirect: le client confirme sur la page de l’opérateur (Orange Money). EnvoyezsuccessUrl, puis redirigez le client verspayment.paymentUrl. L’opérateur ne permet pas de payer entièrement sur votre page.simulated: simulateur, uniquement en Sandbox.
2. Créer le paiement
/v1/paymentsEn-têtes : Authorization: Bearer VOTRE_CLE_SECRETE et Idempotency-Key (obligatoire, une valeur par commande : si votre serveur réessaie, le même paiement est renvoyé au lieu d’en créer un second).
{
"amount": 15000, // en Ariary, entier (nombre ou texte)
"provider": "MVOLA",
"merchantReference": "CMD-2026-0042", // votre numéro de commande
"customer": { "phone": "0340000000" }, // numéro MVola du client (034, 036 ou 038)
"metadata": { "orderId": "42" } // facultatif, renvoyé dans la notification
}{
"payment": {
"id": "cm_exemple_123",
"status": "PENDING",
"amount": "15000",
"fee": "450",
"netAmount": "14550",
"currency": "MGA",
"provider": "MVOLA",
"merchantReference": "CMD-2026-0042"
}
}Gardez payment.id avec votre commande. Le client reçoit la demande MVola en quelques secondes ; affichez par exemple « Validez le paiement sur votre téléphone ».
3. Connaître le résultat
Recommandé : la notification. AIFI PAY envoie à votre URL de notification un événement signé payment.succeeded, payment.failed, payment.cancelled ou payment.expired, comme pour le paiement hébergé. Vérifiez la signature AIFI-Signature avant de valider la commande (voir Vérifier la notification).
En complément : lire le paiement depuis votre serveur, par exemple toutes les 3 secondes pendant que votre page d’attente est affichée :
/v1/payments/:idSUCCEEDED: payé. Validez la commande.FAILED,CANCELLED,EXPIRED: non payé, rien n’a été débité. Proposez de réessayer (avec une nouvelleIdempotency-Key).CREATED,PENDING,PROCESSING: en attente de la validation du client. Ne proposez pas de payer une seconde fois.
Tester en Sandbox
Avec une clé sk_test_…, utilisez "provider": "SANDBOX_SIMULATOR" et "sandboxScenario" (SUCCESS, FAILED, PENDING, EXPIRED, TIMEOUT) pour reproduire chaque cas sans argent réel. Voir Sandbox.
Exemple Node.js (serveur)
// La clé reste côté serveur (variable d'environnement), jamais dans le navigateur.
const API = "https://aifipay.pro";
const KEY = process.env.AIFIPAY_SECRET_KEY;
// Votre page appelle cette route avec le numéro saisi par le client.
app.post("/paiement/mvola", async (req, res) => {
const order = await findOrder(req.body.orderId); // votre commande
const r = await fetch(`${API}/v1/payments`, {
method: "POST",
headers: {
authorization: `Bearer ${KEY}`,
"content-type": "application/json",
"idempotency-key": `order-${order.id}`,
},
body: JSON.stringify({
amount: order.total,
provider: "MVOLA",
merchantReference: order.reference,
customer: { phone: req.body.phone },
}),
});
const data = await r.json();
if (!r.ok) return res.status(400).json({ message: data.error?.message });
await savePaymentId(order.id, data.payment.id);
res.json({ status: data.payment.status }); // votre page affiche l'attente
});
// Votre page interroge cette route pendant l'attente.
app.get("/paiement/:orderId/statut", async (req, res) => {
const paymentId = await getPaymentId(req.params.orderId);
const r = await fetch(`${API}/v1/payments/${paymentId}`, {
headers: { authorization: `Bearer ${KEY}` },
});
const { payment } = await r.json();
res.json({ status: payment.status });
});Erreurs fréquentes
400 VALIDATION_ERROR: numéro absent ou non MVola, montant invalide,successUrlinvalide. Le message indique le champ.401 INVALID_API_KEY: clé absente, révoquée ou d’un autre environnement.409: mêmeIdempotency-Keyavec un contenu différent.400 PROVIDER_REJECTED: MVola a refusé la demande (par exemple solde insuffisant). Rien n’a été débité.429 PROVIDER_RATE_LIMITEDou503: MVola est momentanément saturé ou indisponible. Réessayez un peu plus tard avec la mêmeIdempotency-Key.
Voir aussi Erreurs, Idempotence et Webhooks.