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
- Votre serveur crée une session de paiement avec le montant, votre numéro de commande et deux adresses de retour.
- Vous redirigez votre client vers l’
urlrenvoyée : la page de paiement AIFI PAY (MVola). Le client saisit son numéro et valide avec son code secret. - 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
/v1/checkout/sessionsAdresse : 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.
{
"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
}{
"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.
/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).
{
"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
<?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;<?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)
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 -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.