Guides
Retraits
Un retrait envoie de l'argent depuis votre solde MivaaPay vers un compte Mobile Money : le vôtre, ou celui d'un de vos utilisateurs. C'est le mouvement inverse d'un paiement, et il obéit à des règles différentes.
Un remboursement rend au client ce qu'il a payé, sur un paiement précis. Un retrait sort de l'argent de votre solde vers un bénéficiaire que vous désignez, sans lien avec une transaction entrante.
Deux régimes de bénéficiaire
Le régime se déduit de votre requête : sans phone_number, l'argent part vers
le moyen de reversement validé de votre compte. Avec, il part vers le numéro que vous indiquez.
| Votre compte | Un numéro tiers | |
|---|---|---|
| Requête | phone_number absent |
phone_number et operator fournis |
| Usage | Vous vous versez votre solde. | Vous payez vos propres utilisateurs. |
| Plafonds | Aucun au-delà de votre solde. | Par retrait et cumulé sur 24 heures. |
| Validation | Aucune. | Au-delà du seuil, un accord humain est requis. |
Créer un retrait
curl -X POST https://api.mivaapay.com/api/v1/payouts \
-H "Authorization: Bearer $MIVAAPAY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"operator": "mtn",
"phone_number": "22997000000",
"external_id": "WDR-1042",
"beneficiary_name": "Ada Lovelace",
"reason": "Retrait joueur"
}'
$payout = Http::withToken(env('MIVAAPAY_SECRET_KEY'))
->post('https://api.mivaapay.com/api/v1/payouts', [
'amount' => 5000,
'operator' => 'mtn',
'phone_number' => '22997000000',
'external_id' => 'WDR-1042',
])
->json('data');
const res = await fetch('https://api.mivaapay.com/api/v1/payouts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MIVAAPAY_SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: 5000,
operator: 'mtn',
phone_number: '22997000000',
external_id: 'WDR-1042'
})
});
const { data } = await res.json();
import os, requests
res = requests.post(
'https://api.mivaapay.com/api/v1/payouts',
headers={'Authorization': f"Bearer {os.environ['MIVAAPAY_SECRET_KEY']}"},
json={
'amount': 5000,
'operator': 'mtn',
'phone_number': '22997000000',
'external_id': 'WDR-1042',
},
timeout=30,
)
data = res.json()['data']
La réponse :
{
"success": true,
"data": {
"reference": "pyo_9f2c7a41e8b3d6c05f1a",
"status": "pending",
"amount": 5000,
"fee_amount": 0,
"total_debited": 5000,
"currency": "XOF",
"operator": "mtn",
"phone_number": "229970*****",
"beneficiary_name": "Ada Lovelace",
"external_id": "WDR-1042",
"livemode": true,
"completed_at": null,
"created_at": "2026-08-09T17:41:02+00:00"
}
}
Les frais éventuels s'ajoutent à ce qui vous est prélevé, ils ne sont pas
retenus sur la somme envoyée. C'est l'inverse des encaissements : un joueur qui demande
5 000 XOF doit recevoir 5 000 XOF. Fiez-vous à total_debited pour ce
qui quitte votre solde.
Paramètres
| Champ | Type | Description |
|---|---|---|
amount |
nombre — requis | Ce que reçoit le bénéficiaire. Entre 10 et 1 000 000 XOF, frais compris. |
phone_number |
chaîne |
Numéro du bénéficiaire, indicatif compris et sans + (ex.
22997000000). Omettez-le pour viser votre propre compte enregistré.
|
operator |
chaîne — requis avec phone_number |
Code opérateur. Voir Opérateurs & pays. |
external_id |
chaîne | Votre identifiant. Rend l'appel idempotent — voir plus bas. |
beneficiary_name |
chaîne | Nom du bénéficiaire, pour vos rapprochements. |
reason |
chaîne | Motif transmis à l'opérateur. |
metadata |
objet | Vos données libres, restituées telles quelles. |
Rejouer sans payer deux fois
Un réseau qui coupe après l'envoi ne vous dit pas si le retrait a été créé. Envoyez un
external_id et rejouez sans crainte : le second appel renvoie le retrait
existant au lieu d'en créer un nouveau.
L'identifiant est unique par compte et par environnement : le même
WDR-1042 reste disponible en test et en production.
external_id, aucune protection
Deux appels identiques sans identifiant créent deux retraits, et sortent l'argent deux fois. Sur un décaissement, ce champ n'est pas une commodité.
Suivre un retrait
Un retrait naît pending. Il devient succeeded quand l'opérateur
confirme, ou failed s'il refuse — auquel cas votre solde est recrédité
automatiquement.
| Statut | Signification | Définitif |
|---|---|---|
pending | Transmis à l'opérateur, en attente — ou en attente d'un accord côté MivaaPay. | non |
succeeded | Le bénéficiaire a reçu l'argent. | oui |
failed | Refusé. Votre solde a été recrédité. | oui |
curl https://api.mivaapay.com/api/v1/payouts/pyo_9f2c7a41e8b3d6c05f1a \
-H "Authorization: Bearer $MIVAAPAY_SECRET_KEY"
GET /payouts liste vos retraits, filtrable par status,
from et to.
Être notifié
Deux évènements, signés du secret de l'environnement émetteur comme tous les autres — voir Webhooks.
| Évènement | Émis quand |
|---|---|
payout.succeeded | Le bénéficiaire a reçu l'argent. |
payout.failed | Le retrait a échoué. Votre solde est déjà recrédité. |
POST /payouts répond pending : l'opérateur n'a pas encore
confirmé. Créditez le compte de votre utilisateur sur réception du webhook, pas sur le
201.
Plafonds
Les retraits vers un numéro tiers sont bornés. Ces limites protègent votre solde : une clé secrète dérobée ne peut pas le vider d'un coup.
| Limite | Effet quand elle est franchie |
|---|---|
| Seuil de validation |
Le retrait reste pending le temps qu'un accord soit donné côté MivaaPay.
Votre solde n'est pas débité tant qu'il ne l'est pas. L'appel réussit normalement.
|
| Plafond sur 24 heures |
Le retrait est refusé avec payout_limit_exceeded. Les retraits échoués
n'entrent pas dans ce cumul.
|
Les valeurs applicables à votre compte sont visibles dans votre tableau de bord. Si votre volume les dépasse régulièrement, demandez leur relèvement : elles se règlent compte par compte.
Erreurs propres aux retraits
| Code | HTTP | Cause |
|---|---|---|
insufficient_funds |
402 | Votre solde ne couvre pas le montant, frais compris. Rien n'a été prélevé. |
payout_failed |
402 | L'opérateur a refusé, ou le service de décaissement est indisponible. Rien n'a été prélevé. |
payout_limit_exceeded |
403 | Plafond sur 24 heures atteint. L'argent est là, c'est l'autorisation qui manque. |
invalid_request |
422 | Opérateur inconnu, numéro malformé, montant hors bornes, ou aucun moyen de reversement validé. |
Voir Erreurs pour l'enveloppe commune et les codes partagés.
Essayer sans risque
Avec une clé sk_test_…, aucun argent ne bouge et aucun opérateur n'est contacté.
L'issue est décidée par les quatre derniers chiffres du numéro du bénéficiaire, exactement
comme pour les encaissements — voir Mode test.
| Numéro se terminant par | Résultat |
|---|---|
0000 | succeeded immédiatement. |
0001 | Refus à l'initiation : 402 payout_failed. |
0002 | Reste pending. Pour éprouver vos délais d'attente. |
0003 | failed après coup, avec recrédit. |
| tout autre | pending, puis succeeded au bout de 10 secondes. |
Rien ne sortant, il n'y a rien à borner. Un retrait de test ne déclenche jamais de validation humaine, quel que soit son montant.