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

  1. Votre serveur demande les moyens de paiement disponibles et votre page n’affiche que ceux-là.
  2. Le client choisit MVola et saisit son numéro sur votre page. Votre serveur crée le paiement avec ce numéro.
  3. 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.
  4. 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

GET/v1/payment-methods

Renvoie 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 ».

Réponse 200 (exemple en Production)
{
  "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 dans customer.phone.
  • redirect : le client confirme sur la page de l’opérateur (Orange Money). Envoyez successUrl, puis redirigez le client vers payment.paymentUrl. L’opérateur ne permet pas de payer entièrement sur votre page.
  • simulated : simulateur, uniquement en Sandbox.

2. Créer le paiement

POST/v1/payments

En-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).

Corps de la requête (MVola)
{
  "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
}
Réponse 201 (exemple)
{
  "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 :

GET/v1/payments/:id
  • SUCCEEDED : payé. Validez la commande.
  • FAILED, CANCELLED, EXPIRED : non payé, rien n’a été débité. Proposez de réessayer (avec une nouvelle Idempotency-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)

server.js
// 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, successUrl invalide. Le message indique le champ.
  • 401 INVALID_API_KEY : clé absente, révoquée ou d’un autre environnement.
  • 409 : même Idempotency-Key avec 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_LIMITED ou 503 : MVola est momentanément saturé ou indisponible. Réessayez un peu plus tard avec la même Idempotency-Key.

Voir aussi Erreurs, Idempotence et Webhooks.