Aller au contenu

Transferts

L'objet transfert, les trois 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 trois appels

Il n'en existe aucun autre. L'API ne permet ni de modifier, ni d'annuler, ni de supprimer un transfert. Il n'existe aucun endpoint de lot.

AppelFonction
GET /transfersLister et filtrer vos payouts
POST /transfersCréer un payout et le démarrer immédiatement
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 renvoient cet objet. beneficiary, payment_method et provider sont chargés à chaque fois et sont donc toujours présents.

Réponse · un payout réussi
{
"id": "po_CSUGajfv9xh0XQ5wu2lx",
"reference": null,
"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.
referencenullfacultatif
Toujours null. Lisez l’avertissement ci-dessous avant de vous appuyer sur ce champ.
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
pending, processing, review, succeeded, failed ou cancelled.
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.

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. La valeur par défaut est XAF, pas la devise de votre compte.
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
Validé, puis ignoré. Consultez l'avertissement ci-dessus.
metadataobjectfacultatif
Vos propres clés libres, renvoyées à chaque lecture.

Transmettre le destinataire inline crée le bénéficiaire pendant l'appel. Cette forme convient à une personne que vous ne paierez qu'une fois. 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
L'opérateur qui dessert le numéro, comme cm.mtn. La forme générique 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.

L'ensemble permet de créer en un seul appel un payout qui en aurait autrement nécessité deux.

curl https://api.wajub.com/transfers   -H "Authorization: sk.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"   -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. Un bénéficiaire créé de cette manière est normal : il possède un id, apparaît dans GET /beneficiaries et peut être payé plus tard sans répéter son numéro.

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 pendant 24 heures, par compte et par environnement.

La création de payouts possède sa propre limite de requêtes, qui s'ajoute à celle de votre plan : 20 appels par minute et par compte, quel que soit le plan. Elle s'applique aussi à POST /beneficiaries. Une boucle qui crée un bénéficiaire puis le paie compte donc deux fois.

Réponse reçue et statut initial

Un payout live est créé avec le statut processing et est déjà en cours d'acheminement. pending n'appartient qu'à la sandbox. Même dans celle-ci, ce statut ne dure que l'instant entre l'écriture de la ligne et sa prise en charge par le prestataire simulé. La réponse reçue indique donc déjà processing. Vous ne pouvez l'observer que dans un webhook sandbox transfer.created.

transfer.created est émis à la création de chaque payout, y compris pour un payout retenu pour examen. Statuts et webhooks détaille les quatre événements et le rôle de chaque handler.

cancelled ne provient jamais de l'API. Ce statut apparaît lorsqu'un payout est refusé dans la file de confirmation de la Konsole. C'est le seul statut que votre intégration ne peut pas produire.

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 s'agit de votre premier payoutVotre compte ne possède encore aucun payout réussi dans cette devise
Le montant dépasse le seuil de la plateforme1 000 000 par défaut, comparé au montant quelle que soit la devise
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 vousAu moins cinq fois la moyenne de vos cinquante derniers payouts réussis, dès que vous en avez au moins trois

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'une tâche cron.

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.
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.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"   -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. Si vous transmettez un cursor, meta change entièrement 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.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"

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 ?