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 :
curl "$API_URL/v1/kyc/status" -H "Authorization: Bearer $API_KEY"{
"status": "verified",
"merchant_status": "active",
"withdrawals_blocked": false,
"withdrawals_blocked_reason": null
}status | Signification |
|---|---|
not_started | Aucune vérification commencée. |
pending | Dossier en cours d’examen. |
verified | Identité vérifiée : retraits possibles. |
rejected | Dossier 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
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"}'{ "id": "5e8c…", "rail": "sn_wave", "phone_last_digits": "4567", "verified": false }railest 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_LIMITEDau-delà). - Un numéro déjà enregistré (non retiré) renvoie
200et 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
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"}'{
"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
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éponse | Signification |
|---|---|
201 | Retrait traité : status vaut succeeded ou un statut d’échec. |
202 | Retrait accepté, issue en cours : seuls id et status sont renvoyés. Relire GET /v1/payouts/{id}. |
409 QUOTE_EXPIRED | Devis expiré. Demander un nouveau devis. |
409 QUOTE_INVALID | Devis inconnu ou déjà utilisé. Demander un nouveau devis. |
409 IDEMPOTENCY_CONFLICT | Clé déjà utilisée avec un autre corps. |
409 INSUFFICIENT_FUNDS | Solde disponible insuffisant. Rien n’est prélevé. |
503 CHANNEL_UNAVAILABLE | Retraits 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
| Groupe | Statuts | Signification |
|---|---|---|
| En cours | created, reserving, reserved, queued, dispatch_started, processing | Fonds réservés, versement en préparation ou en cours. |
| Réussi | succeeded | Le numéro a reçu net. Événement payout.succeeded. |
| Échoué | failed_confirmed, released | Versement refusé ; les fonds reviennent au solde disponible. Événement payout.failed. |
| Inconnu | unknown | Issue incertaine, en cours de vérification par 221. |
Un retrait échoué porte failure_reason (absent sur les anciens retraits) :
failure_reason | Signification |
|---|---|
destination_limit | Le portefeuille destinataire a atteint son plafond. Un montant plus petit, un autre numéro ou un compte bancaire reste possible. |
destination_invalid | Compte ou numéro destinataire refusé par l’opérateur. Vérifier le numéro ou le compte. |
provider_unavailable | Service 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
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"}'{
"id": "7a1d…",
"type": "bank",
"bank_code": "SN012",
"rib_last4": "0154",
"holder_name": "Awa Diop",
"verified": false
}| Champ | Format |
|---|---|
rib | 24 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_name | 2 à 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.
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}/resendenvoie un nouveau code (3 par heure au plus).- Code incorrect :
400 INVALID_CODEavecattempts_left. Code expiré :410 CODE_EXPIRED. Trop d’essais :429 TOO_MANY_ATTEMPTS. - Une fois le compte vérifié,
available_atdonne la date du premier retrait, 24 heures plus tard. - En mode test seulement, le code
000000est 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.
curl -X POST "$API_URL/v1/payout-quotes" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"destination_id": "7a1d…", "amount": "300000"}'{
"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"
}debitsrépartitamountentre les soldes Wave et Orange Money du Sénégal, au prorata de leur solde disponible.delayannonce le délai d’arrivée.- Le retrait se confirme comme pour un numéro mobile :
POST /v1/payoutsavec lequote_hashet uneIdempotency-Key.
Frais, minimum et plafonds
Le taux du palier qui contient le montant s’applique à la totalité du montant.
| Montant du retrait | Frais |
|---|---|
| 200 000 à 499 999 F CFA | 5 % |
| 500 000 à 999 999 F CFA | 4 % |
| 1 000 000 à 2 499 999 F CFA | 3 % |
| 2 500 000 à 5 000 000 F CFA | 2,5 % |
GET /v1/fees publie ces paliers dans bank_tiers (from, to, bps), ainsi que bank_min, bank_max et bank_monthly_cap.
| Limite | Valeur | Erreur au-delà |
|---|---|---|
| Minimum par retrait | 200 000 F CFA | 400 BELOW_MINIMUM, avec min_amount, max_amount et fields.amount. |
| Maximum par retrait | 5 000 000 F CFA | 400 ABOVE_MAXIMUM, avec min_amount, max_amount et fields.amount. |
| Total par mois | 10 000 000 F CFA | 403 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.