Transferts
L'objet transfert, les quatre appels et les conditions qui déterminent son exécution.
Un transfert retire des fonds de votre solde pour les envoyer vers le portefeuille Mobile Money d'une autre personne. Vous donnez à Wajub un montant et un destinataire. Wajub réserve les fonds, choisit une route, puis remet le payout à un opérateur. Transferts et payouts présente toute la section, le coût d'un payout et les conditions nécessaires à son exécution. Cette page sert de référence pour l'objet lui-même.
Les payouts nécessitent une clé secrète. Une clé publique renvoie 406 Private Key Required avant
toute validation. Un navigateur est refusé encore plus tôt. Une clé restreinte fonctionne aussi si
ses autorisations couvrent les transferts.
Les quatre appels
Il n'en existe aucun autre. L'API ne permet ni de modifier, ni d'annuler, ni de supprimer un transfert.
| Appel | Fonction |
|---|---|
GET /transfers | Lister et filtrer vos payouts |
POST /transfers | Créer un payout et le démarrer immédiatement |
POST /transfers/batch | Créer jusqu'à 500 payouts en un appel, chaque ligne démarrant séparément |
GET /transfers/{id} | Récupérer un payout |
{id} correspond à la chaîne po_ renvoyée par l'API. Dans la sandbox, elle commence par
po_test_, ce qui permet de distinguer les deux environnements dans un log.
L'objet transfert
Les trois appels portant sur un seul transfert renvoient cet objet, et chaque ligne acceptée d'un lot
le porte sous transfer. beneficiary et payment_method sont chargés à chaque fois et sont donc
toujours présents, y compris un bénéficiaire supprimé depuis. provider aussi, sauf sur un payout
sandbox à sa création : il vaut null jusqu'à ce que le prestataire sandbox le prenne, puis system
(Wajub).
Certains champs ne fonctionnent pas comme leur nom le suggère. L'un d'eux constitue un piège.
idstringfacultatifpo_ en live et po_test_ dans la sandbox.referencestringfacultatifnull si vous n'en avez envoyé aucun.provider_referencestringfacultatifamountnumberfacultatif10000 représente dix mille francs, pas cent.statusenumfacultatifprocessing, review, succeeded, failed ou cancelled en live. pending ne dure qu'un instant en sandbox ; en live, seul un ancien payout du Dashboard qui attend encore votre confirmation le porte, jusqu'à ce que vous le confirmiez ou le rejetiez dans le Dashboard. cancelled, un payout arrêté avant son envoi, à partir de la version d'API 2026-10-12 : failed avant.completebooleanfacultatifsucceeded. Un payout échoué est terminé mais renvoie toujours false. Ce champ indique donc le résultat, pas la fin du payout.sourceenumfacultatifapi pour toute création par votre intégration, dashboard ou recurring pour un payout lancé depuis la Konsole.metadataobjectfacultatiffailure_reasonstringfacultatifstatus vaut failed, avec failure_message. Ce champ est absent pour tous les autres statuts, et non défini à null.ledger_balance_currencystringfacultatifmetadata.settlement.beneficiaryobjectfacultatifid, name, phone, email et deleted. Un bénéficiaire supprimé après le payout reste présent avec deleted: true, et son relevé reste lisible.reference vous appartient, et n'est pas unique
POST /transfers enregistre la reference que vous envoyez, jusqu'à 255 caractères, et la
renvoie à chaque lecture et dans chaque webhook. Wajub ne contrôle jamais son unicité : deux
payouts peuvent porter la même référence, utilisez donc id lorsque vous devez identifier un
payout avec certitude. La recherche dans la liste ne la lit pas non plus, seulement l'id po_ et
la description.
Wajub écrit aussi dans metadata. Un payout live sur le circuit Wajub reçoit un objet reservation
qui indique les fonds retenus et l'heure de la réservation. Il reçoit aussi un objet settlement
si les fonds viennent d'un portefeuille dans une autre devise. Lisez vos propres clés par leur nom
au lieu de supposer que tout l'objet vous appartient.
Créer un transfert
https://api.wajub.com/transfersbeneficiarystring | objectobligatoireamountnumberobligatoire0.01 et dans les plafonds définis pour la devise.currencystringfacultatifdéfaut : XAF2026-10-12. Avant, un payout sans currency part en XAF, pas dans la devise de votre compte : envoyez-la toujours.descriptionstringfacultatifreasonstringfacultatifreferencestringfacultatifmetadataobjectfacultatifLe destinataire se met dans beneficiary, et il y a plusieurs façons de le désigner. Choisissez le
cas qui correspond à ce que vous avez.
| Vous avez | À envoyer dans beneficiary | Cas |
|---|---|---|
| Un bénéficiaire enregistré plus tôt | Son id, en chaîne : "ben_…" | Un bénéficiaire enregistré |
| Les coordonnées du bénéficiaire, pour un premier payout | Un objet : name, channel, phone ou account_number | Un nouveau destinataire |
| Ces coordonnées, pour quelqu'un déjà enregistré | Le même objet | Quelqu'un déjà enregistré |
| Un numéro camerounais, sans son opérateur | L'objet, avec channel: "cm.mobile" | Un numéro sans opérateur |
| Un numéro de compte Wajub | L'objet, avec channel: "wajub" | Un autre compte Wajub |
Dans tous les cas, chaque payout a besoin de sa propre Idempotency-Key : un bénéficiaire
enregistré est une destination, pas une protection contre le double paiement.
Un bénéficiaire enregistré
Envoyez l'id en chaîne. Le reste du payout ne change pas.
Seul l'id est reconnu, dans l'environnement de votre clé : un bénéficiaire sandbox n'existe pas
pour une clé live. Un numéro de téléphone ou une adresse e-mail ne le sont pas, même s'ils
identifient le bénéficiaire ailleurs dans l'API. Chaque refus est un 422 avec un error_code :
| Quand | error_code | Message |
|---|---|---|
Aucun de vos bénéficiaires n'a cet id (numéro de téléphone ou e-mail compris), ou il a été supprimé | beneficiary_not_found | The selected beneficiary is invalid. |
| Il est désactivé | beneficiary_inactive | This beneficiary is deactivated. Reactivate it, or pay another beneficiary. |
| Il a été ajouté depuis la Konsole | beneficiary_not_allowed | This beneficiary was added from the Konsole dashboard and cannot be paid out via the API. |
Ce dernier refus est voulu : une clé divulguée ne doit pas pouvoir payer une destination que votre intégration n'a jamais créée.
Un nouveau destinataire, créé par le payout
Envoyez les coordonnées du bénéficiaire sous forme d'objet : le bénéficiaire est créé pendant l'appel. C'est ce qu'il faut pour un premier payout, ou pour une personne que vous ne paierez qu'une fois : un appel au lieu de deux. Les champs appartiennent au bénéficiaire et sont tous décrits dans Bénéficiaires.
beneficiary.namestringobligatoirebeneficiary.channelstringobligatoirecm.mtn ou ke.mpesa. Au Cameroun uniquement, cm.mobile détermine l'opérateur à partir du numéro.beneficiary.phonestringfacultatifaccount_number.beneficiary.account_numberstringfacultatifbeneficiary.emailstringfacultatifbeneficiary.country_codestringfacultatifbeneficiary.notestringfacultatifcurl https://api.wajub.com/transfers \
-H "Authorization: sk.xxxxxxxxxxxxxxxx…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: payout-892" \
-d '{
"amount": 10000,
"currency": "XAF",
"description": "Supplier payment #892",
"metadata": { "vendor_id": "v_892" },
"beneficiary": {
"name": "Aminata Diallo",
"channel": "cm.mtn",
"phone": "+237670000000"
}
}'La réponse est 201 Created, et le beneficiary.id du transfert est celui du nouveau bénéficiaire.
C'est un bénéficiaire normal : il apparaît dans GET /beneficiaries, beneficiary.created est
envoyé pour lui, et le prochain payout à cette personne peut envoyer cet id au lieu des
coordonnées.
Un destinataire déjà enregistré, renvoyé inline
Envoyer les coordonnées d'une personne déjà enregistrée ne crée pas de second bénéficiaire. Quand un
de vos bénéficiaires actifs utilise déjà ce numéro sur ce canal, le payout va vers lui : le
beneficiary.id du transfert est celui qui existe, et les name, email et note que vous venez
d'envoyer sont ignorés, pas écrits à la place des valeurs enregistrées. Pour les changer, utilisez
PUT /beneficiaries/{id}.
Un numéro sans son opérateur
Un payout désigne son canal, et le Cameroun est la seule exception : quand vous avez un numéro
camerounais sans son opérateur, envoyez cm.mobile avec phone. L'opérateur, cm.mtn ou
cm.orange, est déduit du numéro : en live d'après le plan de numérotation du pays, en sandbox
d'après les préfixes de test. Un numéro qui ne correspond à aucun des deux est refusé avec un 422
sur channel et phone, au lieu d'être envoyé au hasard.
Aucun autre pays n'a de canal générique : ke.mobile, eg.mobile ou tout autre {country}.mobile
est refusé comme un canal inconnu. Indiquez le canal de l'opérateur, comme ke.mpesa ou
eg.vodafone ; Prestataires de payout les liste. Au Cameroun aussi,
nommez l'opérateur quand vous le connaissez : cm.mobile sert quand vous n'avez que le numéro.
Un autre compte Wajub
Pour payer quelqu'un qui a un compte Wajub, envoyez channel: "wajub" et son numéro de compte à
11 chiffres dans account_number. L'argent passe de votre solde au sien immédiatement, sans réseau
entre les deux, et le payout est gratuit. Le compte doit exister, ne peut pas être le vôtre, ni un
compte que Wajub a gelé. Payer un autre compte Wajub
donne le détail.
Créer d'abord, payer ensuite
Vous pouvez aussi créer le bénéficiaire seul avec POST /beneficiaries, puis le payer par son id.
C'est deux appels au lieu d'un, et cela vaut la peine quand vous voulez :
- vérifier le nom que l'opérateur connaît pour un wallet avant que l'argent ne parte, avec
POST /beneficiaries/{id}/verify; - montrer le bénéficiaire dans votre propre interface, ou le faire approuver, avant le premier payout.
Ce que l'API ne peut pas payer
- Un bénéficiaire ajouté depuis la Konsole, compte de versement compris. Payez-le depuis le Dashboard.
- Un compte bancaire. L'API ne crée que des bénéficiaires Mobile Money, wallet adressé par
téléphone (comme Djamo) et compte Wajub. Un payout bancaire passe par un
compte de versement
wire, créé et payé depuis le Dashboard. - Le bénéficiaire du marchand ou d'une autre plateforme, via
X-Sync. Une plateforme qui paie pour un compte connecté ne paie que les bénéficiaires de cette connexion. - Un destinataire inline dans un lot. Chaque ligne de
POST /transfers/batchdésigne un bénéficiaire enregistré par sonid; la forme objet sert aux payouts unitaires.
Les montants utilisent l'unité principale
Le champ amount d'un transfert utilise l'unité principale de la devise et accepte les décimales.
10000 envoie dix mille francs. Une facture utilise au contraire des lignes en
unités entières de la devise. Ne réutilisez donc pas une conversion de l'un vers l'autre.
Chaque devise possède un minimum et un maximum, contrôlés avant toute autre opération. Dépasser
l'un de ces plafonds renvoie une erreur 422 qui indique les deux valeurs.
| Devise | Minimum | Maximum |
|---|---|---|
XAF XOF | 25 | 2 000 000 |
NGN | 100 | 999 999,99 |
RWF | 500 | 999 999 999 |
UGX | 1 000 | 999 999 999 |
GHS KES USD EUR | 0,50 | 999 999,99 |
Ces valeurs constituent les plafonds de la plateforme pour un appel. Les autres plafonds vous sont propres : le plafond quotidien défini par vos soins, la limite liée à votre palier KYC et le plafond par transfert de l'opérateur. Transferts et payouts présente toute la séquence.
L'idempotence est facultative et constitue votre seule protection
Envoyez un en-tête Idempotency-Key. Une nouvelle tentative du même appel renverra alors le
transfert d'origine au lieu d'envoyer les fonds deux fois. Si vous l'omettez, rien n'empêche un
doublon. Il n'existe aucune fenêtre, aucune déduplication selon le montant et aucune annulation de
repli.
La clé accepte au maximum 128 caractères parmi les lettres, les chiffres et . _ : -. Toute autre
valeur renvoie une erreur 422 sur le seul format. Elle est conservée pour toujours, par compte et
par environnement : une clé qui a créé un transfert renvoie toujours ce transfert, et n'en envoie
jamais un second.
Définition d'un même appel, et pendant combien de temps
Pendant 24 heures, réutiliser une clé avec un amount, une currency, un beneficiary, une
description, une reason, une reference ou des metadata différents renvoie 422 This Idempotency-Key was already used with a different request payload. Ensuite, plus rien n'est
comparé : la clé répond 201 avec le transfert d'origine, quoi que dise le nouveau corps, et rien
n'est envoyé. Une clé est consommée pour toujours. Dérivez-la de l'élément payé, comme une ligne
de commande ou un traitement de paie avec sa période. Une même clé ne pourra ainsi jamais décrire
deux payouts différents : salaire-amina réutilisée le mois suivant ne paie personne.
La création de payouts possède sa propre limite de requêtes, qui s'ajoute à celle de votre plan :
20 appels par minute, par compte et par environnement, quel que soit le plan, partagés par
POST /transfers, POST /transfers/batch, POST /accounts/{id}/payouts, POST /beneficiaries, POST /beneficiaries/{id}/receivables et POST /beneficiaries/{id}/receivables/{receivable}/settle. Une boucle qui crée un bénéficiaire puis le paie compte donc deux fois.
Sous une forte charge simultanée, un payout live peut être refusé avec 429, error_code: payout_queue_busy et un en-tête Retry-After. Rien n'a été créé : renvoyez-le après ce délai, avec
la même Idempotency-Key. Une ligne de lot dans ce cas est refusée avec le même code et peut
repartir avec le lot.
Réponse reçue et statut initial
Un payout live est créé avec le statut processing et est déjà en cours d'acheminement, ou avec le
statut review lorsqu'une protection le retient, jamais avec pending : en live, seul un ancien
payout du Dashboard qui attend encore votre confirmation porte pending. Confirmé, il est contrôlé
comme un nouveau payout, ses fonds sont réservés, et il peut être retenu pour
vérification (review) pour les mêmes raisons qu'un nouveau payout, ou
partir en processing ; rejeté, il finit cancelled (failed avant la version d'API 2026-10-12).
En sandbox, ce statut ne dure qu'un instant. La réponse reçue indique donc déjà processing, tout
comme le webhook transfer.created. Un payout que vous créez n'est jamais pending quand vous le
lisez.
transfer.created est émis à la création de chaque payout, y compris pour un payout retenu pour
examen. Statuts et webhooks détaille les cinq
événements et le rôle de chaque handler.
Un payout arrêté avant tout envoi, rejeté dans le Dashboard ou annulé par Wajub, se termine en
cancelled, avec merchant_rejected ou cancelled_by_wajub pour failure_reason. Ce statut est
arrivé avec la version d'API 2026-10-12 : les versions antérieures le lisent en failed. Un payout
que Wajub refuse après un examen se termine en failed, avec admin_rejected.
Raisons de la retenue d'un payout live
Un payout live qui déclenche l'un des contrôles suivants est créé avec le statut review au lieu de
processing. Ses fonds sont réservés, aucun opérateur ne l'a reçu et il attend que Wajub le libère
ou le refuse.
| Motif de retenue | Signification |
|---|---|
| Il fait partie de vos premiers payouts | Les premiers payouts live de votre compte peuvent être retenus pour examen : planifiez-les |
| Le montant dépasse un seuil d'examen de la plateforme | Les payouts importants sont examinés avant leur départ |
| Le montant dépasse votre propre seuil | Le montant d'approbation défini dans Transfer security settings |
| Vous avez demandé la confirmation des payouts | Le réglage de confirmation, sauf pour le montant exempté par votre approbation automatique |
| Le montant est inhabituel pour vous | Le montant est inhabituel pour votre compte |
| Wajub a restreint votre compte | Une restriction comme l'approbation manuelle ou un délai de payout retient chaque payout live, quel que soit son montant, jusqu'à sa levée |
Un payout retenu quitte finalement review dans l'une de deux directions. S'il est libéré, il passe
à processing et continue normalement. S'il est refusé, il passe à failed avec failure_reason: admin_rejected, puis la réservation revient dans votre solde disponible.
Vous ne pouvez ni lister ni débloquer les payouts en cours d'examen
GET /transfers?status=review renvoie une erreur 422. Le filtre accepte les cinq autres statuts,
mais pas celui-ci. Pour voir un payout retenu, récupérez-le directement ou parcourez une liste non
filtrée. Aucun endpoint ne permet de l'approuver, de l'annuler ou de le relancer. Le webhook
transfer.created vous informe de la retenue. transfer.processing ou transfer.failed vous
informe de sa résolution. Attendez ces événements au lieu d'utiliser le polling.
Les payouts sandbox ne sont jamais retenus. Vous ne pouvez pas reproduire ce comportement avant le passage en live. Votre premier payout live mérite donc une surveillance humaine plutôt qu'un script sans surveillance.
En cas d'échec
Un payout échoué contient failure_reason, un code court, et failure_message, une phrase. Ces deux
champs sont absents pour tous les autres statuts. Ne les lisez qu'après avoir vérifié le statut.
Wajub écrit lui-même le code lorsque le payout n'a jamais atteint un opérateur. Après cette étape,
le code correspond à la valeur renvoyée par l'opérateur, ou à la valeur littérale failed si sa
réponse est inutilisable. Considérez ce champ comme un libellé à consigner dans vos logs et à
présenter à une personne, pas comme une enum sur laquelle baser vos branches.
failure_reason | Problème rencontré |
|---|---|
invalid_recipient | La destination est introuvable ou le bénéficiaire a perdu son canal |
no_provider | Aucune route de payout n'était disponible pour ce canal et cette devise |
provider_rejected | L'opérateur a refusé le payout et en indique la raison dans failure_message |
team_restricted | Une restriction de payout a été ajoutée à votre compte entre la création et l'exécution |
admin_rejected | Un payout retenu a été refusé pendant l'examen |
failed, ou le propre code d'un opérateur | Le payout a atteint un opérateur et a échoué |
La sandbox fait exception. Son vocabulaire se limite à failure, network_error et
invalid_recipient, chacun associé à un numéro de test.
Pour un payout sur le circuit Wajub, un échec libère la réservation. Le principal et l'estimation
des frais reviennent dans available, sans aucun prélèvement. Vous pouvez recréer le payout avec
une nouvelle clé d'idempotence.
Tester le payout
Les payouts sandbox puisent dans votre solde sandbox et se terminent automatiquement environ deux secondes plus tard. Le résultat n'est pas aléatoire. Il dépend du numéro du destinataire, qui doit être un numéro de test reconnu. Sinon, l'appel est immédiatement refusé.
| Numéro | Résultat |
|---|---|
+237670000000 | succeeded |
+237670000001 | Refus à la création avec 422, le transfert n'est jamais créé |
+237670000002 | failed, motif failure |
+237670000003 | failed, motif network_error |
+237670000004 | failed, motif invalid_recipient |
Le suffixe compte, pas le préfixe. Ces cinq terminaisons fonctionnent donc après tous les préfixes
d'opérateurs connus de la sandbox. +2250700000002 échoue comme +237670000002. Des zéros initiaux
sont tolérés entre le préfixe et le suffixe.
Des fonds insuffisants ne produisent pas un payout échoué
…0001 représente un destinataire dont le portefeuille ne peut pas recevoir les fonds. La
sandbox renvoie une erreur 422 avant toute création. Il n'existe aucun transfert à récupérer ni
aucun webhook. Pour tester un handler d'échec, utilisez plutôt …0002.
Les payouts sandbox entraînent 2 % de frais de plateforme ajoutés au montant. Ces frais ne sont pas rendus en cas d'échec. Ce taux propre à la sandbox ne correspond pas à la tarification live.
Lister les transferts
https://api.wajub.com/transfersstatusenumfacultatifpending, processing, succeeded, failed ou cancelled. Pas review. Avant la version d'API 2026-10-12, failed renvoie aussi les payouts annulés, que cette version lit en failed.searchstringfacultatifreason, ainsi que le nom et le numéro du bénéficiaire.per_pageintegerfacultatifdéfaut : 25cursorstringfacultatifnext_cursor d'une page précédente. L'envoyer fait passer toute la réponse en mode curseur.Il n'existe aucun filtre par date ou par montant. Une tâche de rapprochement parcourt donc la liste
et applique son propre filtre sur created_at.
curl -G https://api.wajub.com/transfers \
-H "Authorization: sk.xxxxxxxxxxxxxxxx…" \
-d status=failed \
-d per_page=50La liste est triée par date de création, de la plus récente à la plus ancienne. Vous ne pouvez pas
modifier cet ordre. Par défaut, elle utilise une pagination par numéro de page. meta contient alors
current_page, last_page, per_page et total, ainsi que le next_cursor et le has_more qui
permettent de continuer avec un curseur. Si vous transmettez ce next_cursor comme cursor, meta
change de forme et contient per_page, next_cursor, prev_cursor et has_more. Choisissez un
mode et écrivez votre boucle en conséquence. Un handler qui lit last_page casse dès qu'un curseur
est ajouté.
Récupérer un transfert
https://api.wajub.com/transfers/{id}Seul l'id po_ permet de retrouver un transfert. Un transfert appartenant à un autre compte ou à
l'autre environnement renvoie 404 Transfer Not Found, sans révéler son existence ailleurs.
curl https://api.wajub.com/transfers/po_CSUGajfv9xh0XQ5wu2lx \
-H "Authorization: sk.xxxxxxxxxxxxxxxx…"La récupération sert au rapprochement et au support, pas au suivi d'un payout. Attendez plutôt le webhook. Un payout qui atteint un opérateur peut y rester plusieurs minutes. Le polling ne vous apprend rien que l'événement ne vous aurait pas indiqué plus tôt.
Pages associées
- Démarrage rapideVotre premier payout de test, de bout en bout.
- Prestataires et canauxTous les slugs de canaux, classés par pays et par opérateur.
- Statuts et webhooksLes cinq événements et le rôle de chaque handler.
- BénéficiairesEnregistrez une destination une fois, puis payez-la par son id.
- Solde et règlementsLes fonds disponibles et ceux déjà réservés.