Paiement hébergé (Checkout)

La façon la plus simple d’accepter MVola sur votre site : votre client paie sur la page AIFI PAY, puis revient sur votre site.

Comment ça marche

  1. Votre serveur crée une session de paiement avec le montant, votre numéro de commande et deux adresses de retour.
  2. Vous redirigez votre client vers l’url renvoyée : la page de paiement AIFI PAY (MVola). Le client saisit son numéro et valide avec son code secret.
  3. Le paiement confirmé, le client revient automatiquement sur votre successUrl, et AIFI PAY envoie une notification signée à votre URL de notification. Vous validez alors la commande.

Vous n’avez pas de formulaire de paiement à construire ni de numéro de téléphone à collecter.

Vous préférez garder votre propre page de paiement ? Voir l’intégration directe : même API, même clé, mêmes notifications.

Ce qu’il vous faut

  • Une clé API secrète (Tableau de bord → Clés API). Une clé par boutique et par environnement. Elle reste sur votre serveur : ne la mettez jamais dans le code du navigateur ou d’une application mobile.
  • Une URL de notification et son secret de signature (même page).
  • Pour encaisser de l’argent réel : une clé Production (compte vérifié). En Sandbox, rien n’est débité.

1. Créer une session

POST/v1/checkout/sessions

Adresse : https://aifipay.pro/v1/checkout/sessions, en-tête Authorization: Bearer VOTRE_CLE_SECRETE. L’en-tête Idempotency-Key (facultatif mais conseillé) évite de créer deux sessions pour la même commande si votre serveur réessaie.

Corps de la requête
{
  "amount": "15000",                      // en Ariary, entier (texte ou nombre)
  "reference": "CMD-2026-0042",           // votre numéro de commande (obligatoire)
  "description": "Commande CMD-2026-0042", // affiché au client (facultatif)
  "successUrl": "https://votre-site/paiement/succes",
  "cancelUrl": "https://votre-site/panier",
  "expiresInMinutes": 60,                 // 5 à 1440, 60 par défaut
  "metadata": { "clientId": "1234" }      // facultatif, renvoyé dans la notification
}
Réponse (201)
{
  "id": "cmv2…",
  "object": "checkout_session",
  "status": "open",
  "url": "https://aifipay.pro/pay/…",     // redirigez votre client ici
  "reference": "CMD-2026-0042",
  "amount": "15000",
  "currency": "MGA",
  "environment": "PRODUCTION",
  "expiresAt": "2026-10-11T19:50:00.000Z",
  "payment": null
}

2. Rediriger le client

Enregistrez l’id de la session avec votre commande, puis redirigez le client vers url (redirection HTTP 303 ou lien « Payer avec MVola »). Une session ne peut être payée qu’une fois.

3. Retour sur votre site

Après un paiement réussi, le client revient sur successUrl avec ?session_id=…&reference=…. S’il abandonne, le lien « Retour sur le site » le ramène sur cancelUrl.

GET/v1/checkout/sessions/{id}

status vaut open, paid, expired ou closed. Quand il vaut paid, payment.status est SUCCEEDED et payment.netAmount indique ce qui est ajouté à votre solde (montant − 3 %).

4. Notification de paiement

AIFI PAY envoie un POST JSON à votre URL de notification quand le paiement réussit, échoue, est annulé, expire ou est remboursé. En cas d’échec de votre serveur, l’envoi est retenté (1 min, 5 min, 30 min, 2 h, 6 h, 12 h).

Exemple : payment.succeeded
{
  "id": "evt_…",
  "object": "event",
  "type": "payment.succeeded",
  "created": "2026-10-10T18:44:20.000Z",
  "environment": "PRODUCTION",
  "data": {
    "payment": {
      "id": "cmv2…",
      "status": "SUCCEEDED",
      "amount": "15000",
      "fee": "450",
      "netAmount": "14550",
      "currency": "MGA",
      "provider": "MVOLA",
      "operatorReference": "9185680971",   // n° du reçu client (Ref du SMS MVola)
      "paymentLinkId": "cmv2…",            // = id de la session
      "metadata": {
        "checkoutSessionId": "cmv2…",
        "reference": "CMD-2026-0042",       // votre numéro de commande
        "orderMetadata": { "clientId": "1234" }
      },
      "succeededAt": "2026-10-10T18:44:20.000Z"
    }
  }
}

operatorReference est le numéro que le client voit sur son reçu (« Ref » du SMS MVola, « ID de transaction » Orange Money) : gardez-le avec la commande pour le service client. Détails : Paiements → Référence opérateur.

Répondez 200 rapidement. Un même événement peut arriver deux fois : utilisez son id pour ne traiter la commande qu’une fois.

Vérifier la signature

Chaque notification porte l’en-tête AIFI-Signature: t=HORODATAGE,v1=SIGNATURE. v1 = HMAC-SHA256 (clé : votre secret whsec_…) de t + "." + corps brut. Comparez sur le corps brut, avant de le décoder, et refusez les horodatages de plus de 5 minutes.

Exemple complet — PHP

checkout.php — créer la session et rediriger
<?php
$secretKey = getenv('AIFIPAY_SECRET_KEY'); // sk_live_… ou sk_test_…
$order = ['id' => 'CMD-2026-0042', 'total' => 15000];

$ch = curl_init('https://aifipay.pro/v1/checkout/sessions');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Authorization: Bearer ' . $secretKey,
    'Content-Type: application/json',
    'Idempotency-Key: order-' . $order['id'],
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'amount' => (string) $order['total'],
    'reference' => $order['id'],
    'successUrl' => 'https://votre-site/paiement/succes',
    'cancelUrl' => 'https://votre-site/panier',
  ]),
]);
$session = json_decode(curl_exec($ch), true);
curl_close($ch);

// Enregistrez $session['id'] avec la commande, puis :
header('Location: ' . $session['url'], true, 303);
exit;
notification.php — recevoir et vérifier la notification
<?php
$secret = getenv('AIFIPAY_WEBHOOK_SECRET'); // whsec_…
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_AIFI_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $sig); // ['t' => …, 'v1' => …]

$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, $secret);
if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
  http_response_code(400);
  exit('signature invalide');
}

$event = json_decode($body, true);
if ($event['type'] === 'payment.succeeded') {
  $reference = $event['data']['payment']['metadata']['reference'];
  // marquez la commande $reference comme payée (une seule fois par $event['id'])
}
http_response_code(200);

Exemple complet — Node.js (Express)

server.js
import express from "express";
import crypto from "node:crypto";

const app = express();
const SECRET_KEY = process.env.AIFIPAY_SECRET_KEY;       // sk_live_… / sk_test_…
const WEBHOOK_SECRET = process.env.AIFIPAY_WEBHOOK_SECRET; // whsec_…

app.post("/payer/:orderId", async (req, res) => {
  const order = { id: req.params.orderId, total: 15000 };
  const r = await fetch("https://aifipay.pro/v1/checkout/sessions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${SECRET_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `order-${order.id}`,
    },
    body: JSON.stringify({
      amount: String(order.total),
      reference: order.id,
      successUrl: "https://votre-site/paiement/succes",
      cancelUrl: "https://votre-site/panier",
    }),
  });
  const session = await r.json();
  res.redirect(303, session.url);
});

// Corps brut obligatoire pour vérifier la signature
app.post("/aifipay/notifications", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("AIFI-Signature") ?? "";
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(`${t}.${req.body}`).digest("hex");
  const valid =
    v1 && expected.length === v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
  if (!valid || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.status(400).send("signature invalide");

  const event = JSON.parse(req.body);
  if (event.type === "payment.succeeded") {
    const { reference } = event.data.payment.metadata;
    // marquez la commande `reference` comme payée (une seule fois par event.id)
  }
  res.sendStatus(200);
});

app.listen(3000);

Tester en ligne de commande

cURL
curl -X POST https://aifipay.pro/v1/checkout/sessions \
  -H "Authorization: Bearer $AIFIPAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: test-001" \
  -d '{"amount":"1000","reference":"TEST-001","successUrl":"https://votre-site/ok","cancelUrl":"https://votre-site/panier"}'

Avant la mise en ligne

  • La clé secrète est dans une variable d’environnement du serveur, jamais dans le navigateur.
  • La commande est validée par la notification signée (ou un GET de la session), pas par le retour du client.
  • Chaque notification n’est traitée qu’une fois (id de l’événement).
  • Testé de bout en bout en Sandbox, puis une fois en Production avec un petit montant.