Docs API

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 retrait n'est pas un remboursement

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 compteUn 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 :

201 Created
{
  "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"
  }
}
Le bénéficiaire reçoit le montant entier

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

ChampTypeDescription
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.

Sans 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.

StatutSignificationDéfinitif
pendingTransmis à l'opérateur, en attente — ou en attente d'un accord côté MivaaPay.non
succeededLe bénéficiaire a reçu l'argent.oui
failedRefusé. Votre solde a été recrédité.oui
Consulter un retrait
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.succeededLe bénéficiaire a reçu l'argent.
payout.failedLe retrait a échoué. Votre solde est déjà recrédité.
N'attendez pas la réponse HTTP pour conclure

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.

LimiteEffet 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

CodeHTTPCause
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 parRésultat
0000succeeded immédiatement.
0001Refus à l'initiation : 402 payout_failed.
0002Reste pending. Pour éprouver vos délais d'attente.
0003failed après coup, avec recrédit.
tout autrepending, puis succeeded au bout de 10 secondes.
Les plafonds ne s'appliquent pas en test

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.