Aller au contenu

Retraits

Vérifier son identité, enregistrer un numéro mobile ou un compte bancaire, obtenir un devis puis retirer le solde disponible.

Un retrait verse le solde disponible du marchand sur un numéro mobile ou, pour le solde du Sénégal, sur un compte bancaire enregistré (voir « Retrait vers un compte bancaire » plus bas). Il se fait en deux temps : un devis (montants figés, aucun mouvement d’argent), puis le retrait.

Prérequis : vérification d’identité

La vérification d’identité conditionne les retraits et l’enregistrement d’un numéro de retrait. Elle porte sur une pièce d’identité et se fait dans le tableau de bord, rubrique Vérification d’identité. 221 examine le dossier ; le résultat apparaît dans la même rubrique.

Vérifier l’état depuis l’API :

Terminal
curl "$API_URL/v1/kyc/status" -H "Authorization: Bearer $API_KEY"
200 · application/json (extrait)
{
  "status": "verified",
  "merchant_status": "active",
  "withdrawals_blocked": false,
  "withdrawals_blocked_reason": null
}
statusSignification
not_startedAucune vérification commencée.
pendingDossier en cours d’examen.
verifiedIdentité vérifiée : retraits possibles.
rejectedDossier refusé ; reason_code donne le motif.

Avant vérification, les routes de retrait répondent 403 KYC_REQUIRED, avec kyc_status.

1. Enregistrer un numéro de retrait

Terminal
curl -X POST "$API_URL/v1/payout-destinations" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rail": "sn_wave", "phone": "77 123 45 67"}'
201 · application/json
{ "id": "5e8c…", "rail": "sn_wave", "phone_last_digits": "4567", "verified": false }
  • rail est un moyen de paiement actif sur le compte (voir Moyens de paiement et pays).
  • Le numéro est un mobile valide dans le pays de ce moyen de paiement, au format local ou E.164.
  • Un numéro se vérifie par un code reçu par e-mail avant le premier retrait, comme un compte bancaire (voir plus bas). Sans cela, 409 DESTINATION_NOT_VERIFIED.
  • 5 ajouts au plus par 24 heures (429 RATE_LIMITED au-delà).
  • Un numéro déjà enregistré (non retiré) renvoie 200 et la même destination, même quand la limite d’ajouts est atteinte : seuls les nouveaux numéros comptent.
  • La réponse ne montre que les quatre derniers chiffres.

Lister : GET /v1/payout-destinations. Retirer un numéro : DELETE /v1/payout-destinations/{id} (204).

2. Obtenir un devis

Terminal
curl -X POST "$API_URL/v1/payout-quotes" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rail": "sn_wave", "destination_id": "5e8c…", "amount": "100000"}'
201 · application/json
{
  "quote_hash": "9b2d…",
  "rail": "sn_wave",
  "destination_id": "5e8c…",
  "amount": "100000",
  "fee": "…",
  "net": "…",
  "expires_at": "2026-10-07T10:02:00Z"
}

amount est le montant prélevé sur le solde ; net = amount − fee est ce que reçoit le numéro. Le devis expire après 2 minutes. Le moyen de paiement du devis est celui du numéro.

3. Retirer

Terminal
curl -X POST "$API_URL/v1/payouts" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: retrait-2026-10-07-1" \
  -H "Content-Type: application/json" \
  -d '{"quote_hash": "9b2d…"}'
RéponseSignification
201Retrait traité : status vaut succeeded ou un statut d’échec.
202Retrait accepté, issue en cours : seuls id et status sont renvoyés. Relire GET /v1/payouts/{id}.
409 QUOTE_EXPIREDDevis expiré. Demander un nouveau devis.
409 QUOTE_INVALIDDevis inconnu ou déjà utilisé. Demander un nouveau devis.
409 IDEMPOTENCY_CONFLICTClé déjà utilisée avec un autre corps.
409 INSUFFICIENT_FUNDSSolde disponible insuffisant. Rien n’est prélevé.
503 CHANNEL_UNAVAILABLERetraits momentanément suspendus sur ce moyen de paiement ; le solde reste intact.

Les fonds sont réservés dès l’acceptation : ils quittent available pour reserved dans Soldes.

Statuts

GroupeStatutsSignification
En courscreated, reserving, reserved, queued, dispatch_started, processingFonds réservés, versement en préparation ou en cours.
RéussisucceededLe numéro a reçu net. Événement payout.succeeded.
Échouéfailed_confirmed, releasedVersement refusé ; les fonds reviennent au solde disponible. Événement payout.failed.
InconnuunknownIssue incertaine, en cours de vérification par 221.

Un retrait échoué porte failure_reason (absent sur les anciens retraits) :

failure_reasonSignification
destination_limitLe portefeuille destinataire a atteint son plafond. Un montant plus petit, un autre numéro ou un compte bancaire reste possible.
destination_invalidCompte ou numéro destinataire refusé par l’opérateur. Vérifier le numéro ou le compte.
provider_unavailableService de paiement momentanément indisponible. Nouvel essai possible plus tard.

Dans les trois cas, les fonds reviennent au solde disponible. Le même champ est dans le corps de payout.failed (voir Webhooks). L’objet retrait porte aussi destination_type : mobile ou bank.

GET /v1/payouts liste les 50 derniers retraits ; un retrait refusé avant envoi y apparaît en not_sent, sans mouvement d’argent.

Retrait à l’issue inconnue

Un retrait unknown n’est jamais renvoyé ni libéré d’office : un double versement est exclu. Les fonds restent réservés jusqu’à la preuve de l’issue. Ne pas créer un second retrait pour compenser ; suivre GET /v1/payouts/{id} ou le webhook.

Reçu

GET /v1/payouts/{id}/receipt renvoie le reçu d’un retrait réussi : id, amount, fee, net, rail, rail_label, destination_last_digits, succeeded_at et livemode. La route répond 404 tant que le retrait n’a pas réussi.

Retrait vers un compte bancaire

Le solde du Sénégal (Wave et Orange Money) peut aussi être viré sur un compte bancaire de la zone UEMOA. Le service de paiement exécute le virement, qui arrive en 3 à 5 jours ouvrés.

Enregistrer un compte bancaire

Terminal
curl -X POST "$API_URL/v1/payout-destinations" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "bank", "rib": "SN012 01234 012345678901 54", "holder_name": "Awa Diop"}'
201 · application/json (extrait)
{
  "id": "7a1d…",
  "type": "bank",
  "bank_code": "SN012",
  "rib_last4": "0154",
  "holder_name": "Awa Diop",
  "verified": false
}
ChampFormat
rib24 caractères : code banque (code pays et 3 chiffres, par exemple SN012), code guichet (5 chiffres), numéro de compte (12 caractères), clé (2 chiffres). Les espaces sont acceptés. Codes pays : BJ, BF, CI, GW, ML, NE, SN, TG.
holder_name2 à 70 caractères : lettres, espaces, apostrophe, tiret et point.

La clé est contrôlée : clé = 97 − ((code banque, code guichet et numéro de compte, lettres converties en chiffres) × 100 modulo 97). Conversion : A à I valent 1 à 9, J à R valent 1 à 9, S à Z valent 2 à 9. Un RIB refusé renvoie 400 INVALID_REQUEST, avec fields.rib ou fields.holder_name.

La liste (GET /v1/payout-destinations) renvoie pour un compte bancaire type, bank_code, rib_last4 (les 4 derniers caractères seulement), holder_name, verified, available_at et verification. Un numéro mobile a type: "mobile", ou pas de type sur les anciennes données.

Vérifier le compte

Un code à 6 chiffres est envoyé par e-mail au propriétaire du compte marchand. Il vaut 10 minutes, avec 5 essais.

Terminal
curl -X POST "$API_URL/v1/payout-destinations/7a1d…/verify" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code": "123456"}'
  • POST /v1/payout-destinations/{id}/resend envoie un nouveau code (3 par heure au plus).
  • Code incorrect : 400 INVALID_CODE avec attempts_left. Code expiré : 410 CODE_EXPIRED. Trop d’essais : 429 TOO_MANY_ATTEMPTS.
  • Une fois le compte vérifié, available_at donne la date du premier retrait, 24 heures plus tard.
  • En mode test seulement, le code 000000 est aussi accepté, pour un numéro ou un compte bancaire (un code expiré reste refusé : 410 CODE_EXPIRED). Le mode live le refuse.

Devis et retrait

Le devis et le retrait utilisent les mêmes routes que pour un numéro mobile, avec le destination_id du compte bancaire.

Terminal
curl -X POST "$API_URL/v1/payout-quotes" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"destination_id": "7a1d…", "amount": "300000"}'
201 · application/json
{
  "quote_hash": "c41f…",
  "destination_id": "7a1d…",
  "amount": "300000",
  "fee": "15000",
  "net": "285000",
  "debits": [
    { "rail": "sn_wave", "amount": "180000" },
    { "rail": "sn_orange", "amount": "120000" }
  ],
  "delay": "3-5 business days",
  "expires_at": "2026-10-07T10:02:00Z"
}
  • debits répartit amount entre les soldes Wave et Orange Money du Sénégal, au prorata de leur solde disponible.
  • delay annonce le délai d’arrivée.
  • Le retrait se confirme comme pour un numéro mobile : POST /v1/payouts avec le quote_hash et une Idempotency-Key.

Frais, minimum et plafonds

Le taux du palier qui contient le montant s’applique à la totalité du montant.

Montant du retraitFrais
200 000 à 499 999 F CFA5 %
500 000 à 999 999 F CFA4 %
1 000 000 à 2 499 999 F CFA3 %
2 500 000 à 5 000 000 F CFA2,5 %

GET /v1/fees publie ces paliers dans bank_tiers (from, to, bps), ainsi que bank_min, bank_max et bank_monthly_cap.

LimiteValeurErreur au-delà
Minimum par retrait200 000 F CFA400 BELOW_MINIMUM, avec min_amount, max_amount et fields.amount.
Maximum par retrait5 000 000 F CFA400 ABOVE_MAXIMUM, avec min_amount, max_amount et fields.amount.
Total par mois10 000 000 F CFA403 MONTHLY_LIMIT_REACHED, avec remaining.

Les plafonds des retraits vers un numéro mobile (voir plus bas) ne s’appliquent pas aux virements bancaires.

Statuts d’un virement

Un virement reste processing pendant 3 à 5 jours ouvrés, puis devient succeeded ou failed_confirmed. En cas d’échec, les fonds reviennent au solde disponible. Les événements sont payout.succeeded et payout.failed, comme pour un numéro mobile.

Plafonds des retraits vers un numéro mobile

Valeurs par défaut de la plateforme, jours et mois comptés à l’heure de Dakar : 150 000 F CFA par retrait, 3 retraits et 300 000 F CFA par jour, 1 500 000 F CFA par mois. 221 peut les ajuster pour un marchand. Au-delà : 403 WITHDRAWAL_LIMIT_EXCEEDED, avec limit_type (per_withdrawal, daily_count, daily_amount ou monthly_amount) et limit.

Un compte marchand peut aussi avoir un plafond de retrait journalier ou mensuel, et un moyen de paiement un montant maximal par retrait. Au-delà : 403 MERCHANT_LIMIT_EXCEEDED (avec limit_type et limit) ou 400 INVALID_REQUEST avec le montant maximal dans le message.

Le taux des frais de retrait : voir Frais.