Aller au contenu

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.

AppelFonction
GET /transfersLister et filtrer vos payouts
POST /transfersCréer un payout et le démarrer immédiatement
POST /transfers/batchCré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).

Réponse · un payout réussi
{
"id": "po_CSUGajfv9xh0XQ5wu2lx",
"reference": "supplier-892",
"provider_reference": "MP260912.1431.A47201",
"amount": 10000,
"currency": "XAF",
"description": "Supplier payment #892",
"reason": null,
"metadata": {
"vendor_id": "v_892",
"reservation": {
"amount": 10000,
"currency": "XAF",
"fee_estimate": 100,
"reserved_at": "2026-09-12T14:31:02+00:00"
}
},
"ledger_balance_currency": null,
"sandbox": false,
"status": "succeeded",
"complete": true,
"source": "api",
"provider": {
"slug": "mtn_momo",
"name": "MTN MoMo"
},
"beneficiary": {
"id": "ben_7Kq2mX9vL4tRb3nP8sZc",
"name": "Aminata Diallo",
"phone": "+237670000000",
"email": null
},
"payment_method": {
"id": "pm_3Nq8wR2kL5vT7yD4hB1e",
"account_number": null,
"phone": "+237670000000"
},
"succeeded_at": "2026-09-12T14:31:44+00:00",
"created_at": "2026-09-12T14:31:02+00:00",
"updated_at": "2026-09-12T14:31:44+00:00"
}

Certains champs ne fonctionnent pas comme leur nom le suggère. L'un d'eux constitue un piège.

idstringfacultatif
L'identifiant utilisé par tous les autres appels. po_ en live et po_test_ dans la sandbox.
referencestringfacultatif
Votre propre identifiant, tel que vous l'avez envoyé à la création, ou la référence de la ligne dans un lot. null si vous n'en avez envoyé aucun.
provider_referencestringfacultatif
L'identifiant propre à l'opérateur, renseigné dès que le payout lui parvient. Il vaut null avant cette étape. Le support de l'opérateur du bénéficiaire vous demandera cette valeur.
amountnumberfacultatif
Dans l'unité principale de la devise. 10000 représente dix mille francs, pas cent.
statusenumfacultatif
processing, 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.
completebooleanfacultatif
Vaut true uniquement avec succeeded. Un payout échoué est terminé mais renvoie toujours false. Ce champ indique donc le résultat, pas la fin du payout.
sourceenumfacultatif
api pour toute création par votre intégration, dashboard ou recurring pour un payout lancé depuis la Konsole.
metadataobjectfacultatif
Vos données, avec celles ajoutées par Wajub. Voir ci-dessous.
failure_reasonstringfacultatif
Présent uniquement lorsque status vaut failed, avec failure_message. Ce champ est absent pour tous les autres statuts, et non défini à null.
ledger_balance_currencystringfacultatif
Le portefeuille sandbox débité. Vaut null pour un payout live, où le portefeuille réellement utilisé est enregistré dans metadata.settlement.
beneficiaryobjectfacultatif
id, 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

POSThttps://api.wajub.com/transfers
beneficiarystring | objectobligatoire
L'id d'un bénéficiaire enregistré ou les coordonnées inline du destinataire. Les deux formes sont détaillées ci-dessous.
amountnumberobligatoire
Dans l'unité principale, au moins 0.01 et dans les plafonds définis pour la devise.
currencystringfacultatifdéfaut : XAF
Trois lettres et une devise active. Obligatoire à partir de la version d'API 2026-10-12. Avant, un payout sans currency part en XAF, pas dans la devise de votre compte : envoyez-la toujours.
descriptionstringfacultatif
Texte libre de 255 caractères, utilisable dans la recherche des deux environnements.
reasonstringfacultatif
Un second champ de texte libre de 255 caractères. Il est stocké et renvoyé, sans autre utilisation.
referencestringfacultatif
Jusqu'à 255 caractères. Enregistrée et renvoyée, jamais contrôlée pour son unicité.
metadataobjectfacultatif
Vos propres clés libres, renvoyées à chaque lecture.

Le 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 beneficiaryCas
Un bénéficiaire enregistré plus tôtSon id, en chaîne : "ben_…"Un bénéficiaire enregistré
Les coordonnées du bénéficiaire, pour un premier payoutUn objet : name, channel, phone ou account_numberUn nouveau destinataire
Ces coordonnées, pour quelqu'un déjà enregistréLe même objetQuelqu'un déjà enregistré
Un numéro camerounais, sans son opérateurL'objet, avec channel: "cm.mobile"Un numéro sans opérateur
Un numéro de compte WajubL'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.

Payer un bénéficiaire enregistré
{
"beneficiary": "ben_7Kq2mX9vL4tRb3nP8sZc",
"amount": 15000,
"currency": "XAF",
"description": "Forfait de septembre"
}

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 :

Quanderror_codeMessage
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_foundThe selected beneficiary is invalid.
Il est désactivébeneficiary_inactiveThis beneficiary is deactivated. Reactivate it, or pay another beneficiary.
Il a été ajouté depuis la Konsolebeneficiary_not_allowedThis 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.namestringobligatoire
Le bénéficiaire, tel qu'il doit apparaître sur le payout.
beneficiary.channelstringobligatoire
Le canal de l'opérateur qui dessert le numéro, comme cm.mtn ou ke.mpesa. Au Cameroun uniquement, cm.mobile détermine l'opérateur à partir du numéro.
beneficiary.phonestringfacultatif
Obligatoire sauf si vous transmettez account_number.
beneficiary.account_numberstringfacultatif
L'autre partie de cette paire. L'un des deux champs doit être présent.
beneficiary.emailstringfacultatif
Adresse de contact. Aucun message n'y est envoyé.
beneficiary.country_codestringfacultatif
Deux lettres. Par défaut, les deux premières lettres du slug du canal.
beneficiary.notestringfacultatif
Texte libre de 500 caractères.
curl 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.

Laisser le numéro choisir l'opérateur
{
"amount": 10000,
"currency": "XAF",
"beneficiary": {
"name": "Aminata Diallo",
"channel": "cm.mobile",
"phone": "+237670000000"
}
}

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.

Payer un autre compte Wajub
{
"amount": 25000,
"currency": "XAF",
"beneficiary": {
"name": "Boutique Ada",
"channel": "wajub",
"account_number": "12345678901"
}
}

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/batch désigne un bénéficiaire enregistré par son id ; 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.

DeviseMinimumMaximum
XAF XOF252 000 000
NGN100999 999,99
RWF500999 999 999
UGX1 000999 999 999
GHS KES USD EUR0,50999 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.

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 retenueSignification
Il fait partie de vos premiers payoutsLes premiers payouts live de votre compte peuvent être retenus pour examen : planifiez-les
Le montant dépasse un seuil d'examen de la plateformeLes payouts importants sont examinés avant leur départ
Le montant dépasse votre propre seuilLe montant d'approbation défini dans Transfer security settings
Vous avez demandé la confirmation des payoutsLe réglage de confirmation, sauf pour le montant exempté par votre approbation automatique
Le montant est inhabituel pour vousLe montant est inhabituel pour votre compte
Wajub a restreint votre compteUne 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.

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_reasonProblème rencontré
invalid_recipientLa destination est introuvable ou le bénéficiaire a perdu son canal
no_providerAucune route de payout n'était disponible pour ce canal et cette devise
provider_rejectedL'opérateur a refusé le payout et en indique la raison dans failure_message
team_restrictedUne restriction de payout a été ajoutée à votre compte entre la création et l'exécution
admin_rejectedUn payout retenu a été refusé pendant l'examen
failed, ou le propre code d'un opérateurLe 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éroRésultat
+237670000000succeeded
+237670000001Refus à la création avec 422, le transfert n'est jamais créé
+237670000002failed, motif failure
+237670000003failed, motif network_error
+237670000004failed, 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

GEThttps://api.wajub.com/transfers
statusenumfacultatif
pending, 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.
searchstringfacultatif
Recherche dans l'id et la description. En mode live, elle couvre aussi reason, ainsi que le nom et le numéro du bénéficiaire.
per_pageintegerfacultatifdéfaut : 25
Entre 1 et 100.
cursorstringfacultatif
Le next_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=50

La 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

GEThttps://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.

Que pensez-vous de ce contenu ?