Guides
Suivre un paiement
Créer un paiement ne fait que lancer la demande. Savoir s'il a abouti passe soit par un webhook, soit par une interrogation de l'API. Cette page explique quand utiliser l'un ou l'autre, et comment ne pas livrer une commande qui n'a pas été payée.
Les quatre statuts
L'API expose quatre statuts, et eux seuls. Ils suffisent à décider quoi faire :
| Statut | Signification | Ce que vous faites |
|---|---|---|
pending |
La demande est partie chez l'opérateur, l'issue est inconnue. | Vous attendez. Rien n'est acquis. |
succeeded |
Le client a été débité, l'argent est crédité sur votre compte MivaaPay. | Vous livrez la commande. |
failed |
L'opérateur a refusé : solde insuffisant, refus du client, numéro invalide, délai dépassé. | Vous proposez un nouvel essai. Rien n'a été débité. |
refunded |
Le paiement a été remboursé au client. | Vous annulez la commande. Voir Remboursements. |
succeeded et failed sont définitifs : un paiement validé ne
redevient jamais pending, et un paiement échoué ne devient jamais
succeeded. La seule transition possible après succeeded est
refunded, à votre demande.
Votre back-office affiche davantage d'états (« À reverser », « Transférée »…) :
ils décrivent où en est notre reversement vers vous, pas le paiement du client. Du point
de vue de l'API, ils sont tous succeeded — vous avez été payé, le reste est notre affaire.
Webhooks — la méthode recommandée
Dès qu'un paiement bascule en succeeded ou failed, MivaaPay appelle
votre endpoint. Vous n'attendez rien, ne consommez pas de quota, et êtes notifié en quelques
secondes plutôt qu'au prochain tour de boucle.
payment.succeeded # le client a été débité
payment.failed # l'opérateur a refusé
payment.refunded # le paiement a été remboursé
La configuration de l'endpoint, la vérification de signature et la gestion des rejeux sont détaillées dans Webhooks. C'est la page à lire avant la mise en production.
Interroger l'API
Si vous ne pouvez pas exposer d'URL publique — développement local, application interne — interrogez le paiement directement :
curl https://api.mivaapay.com/api/v1/payments/9f2c1a44-8b0e-4b2e-9b0a-2f1d6e5c7a31 \
-H "Authorization: Bearer $MIVAAPAY_SECRET_KEY"
$payment = Http::withToken(env('MIVAAPAY_SECRET_KEY'))
->get("https://api.mivaapay.com/api/v1/payments/{$reference}")
->json('data');
if ($payment['status'] === 'succeeded') {
// livrer la commande
}
const res = await fetch(`https://api.mivaapay.com/api/v1/payments/${reference}`, {
headers: { Authorization: `Bearer ${process.env.MIVAAPAY_SECRET_KEY}` }
});
const { data: payment } = await res.json();
payment = requests.get(
f'https://api.mivaapay.com/api/v1/payments/{reference}',
headers={'Authorization': f"Bearer {SECRET_KEY}"},
timeout=30,
).json()['data']
Tant qu'un paiement est pending, chaque appel à GET /payments/{reference}
va redemander son état à l'opérateur avant de répondre. Vous obtenez donc l'information la plus
fraîche possible, mais la requête est plus lente qu'une simple lecture en base : prévoyez un
timeout d'au moins 30 secondes côté client.
Chercher par votre propre identifiant
Le même endpoint accepte votre external_id à la place de la référence MivaaPay.
Pratique quand vous n'avez sous la main que votre numéro de commande :
curl https://api.mivaapay.com/api/v1/payments/cmd_1042 \
-H "Authorization: Bearer $MIVAAPAY_SECRET_KEY"
À quelle cadence interroger
L'API accepte 120 requêtes par minute et par adresse IP : une boucle serrée sur plusieurs paiements en parallèle vous fera atteindre la limite. Une cadence qui tient la route :
- Attendez 5 secondes après la création avant la première vérification ;
- puis interrogez toutes les 5 secondes pendant 2 minutes ;
- puis espacez à 30 secondes jusqu'à 10 minutes ;
- au-delà, arrêtez la boucle et traitez le paiement comme abandonné de votre côté.
$deadline = time() + 120;
do {
sleep(5);
$status = Http::withToken(env('MIVAAPAY_SECRET_KEY'))
->get("https://api.mivaapay.com/api/v1/payments/{$reference}")
->json('data.status');
} while ($status === 'pending' && time() < $deadline);
// $status vaut 'succeeded', 'failed', ou encore 'pending' si le client n'a rien fait
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function waitForPayment(reference, timeoutMs = 120000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
await sleep(5000);
const res = await fetch(`https://api.mivaapay.com/api/v1/payments/${reference}`, {
headers: { Authorization: `Bearer ${process.env.MIVAAPAY_SECRET_KEY}` }
});
const { data } = await res.json();
if (data.status !== 'pending') return data;
}
return null; // toujours en attente : à traiter comme non payé
}
import time
def wait_for_payment(reference, timeout=120):
deadline = time.time() + timeout
while time.time() < deadline:
time.sleep(5)
payment = requests.get(
f'https://api.mivaapay.com/api/v1/payments/{reference}',
headers={'Authorization': f"Bearer {SECRET_KEY}"},
timeout=30,
).json()['data']
if payment['status'] != 'pending':
return payment
return None # toujours en attente : à traiter comme non payé
pending
Un paiement resté pending à la fin de votre boucle n'est pas un paiement échoué :
le client peut très bien valider trois minutes plus tard. Ne libérez pas la commande, mais ne
la marquez pas non plus définitivement perdue — laissez le webhook, ou une vérification
différée, trancher.
Lister vos paiements
Pour un rapprochement comptable ou un écran d'historique, GET /payments renvoie la
liste paginée avec des filtres :
curl -G https://api.mivaapay.com/api/v1/payments \
-H "Authorization: Bearer $MIVAAPAY_SECRET_KEY" \
--data-urlencode "status=succeeded" \
--data-urlencode "from=2026-08-01" \
--data-urlencode "to=2026-08-31" \
--data-urlencode "per_page=100"
Filtres disponibles : status, external_id, from,
to, per_page. Les détails sont dans la
référence.
Rapprocher webhook et interrogation
Les deux mécanismes peuvent vous apprendre la même chose deux fois : le webhook arrive pendant que votre boucle tourne encore. C'est normal, et sans conséquence si votre traitement est idempotent.
La règle pratique : stockez la reference avec l'état que vous avez déjà traité,
et ignorez toute notification qui ne change rien. Livrez la commande sur la première transition
vers succeeded, jamais sur les suivantes.