Statuts et webhooks
La signification du statut d'un payout, les événements émis et leur traitement.
201 Created confirme la réception d'une requête, pas le paiement d'une personne. Entre cette
réponse et l'arrivée des fonds sur un téléphone se trouvent un opérateur, une file d'attente et,
parfois, une personne.
Le statut indique où se trouve un payout. Le webhook vous informe de son évolution. Cette page explique la signification de chaque statut pour vos fonds, les événements émis ou non, et les conditions permettant à un handler de rester correct après une troisième livraison en double.
Les six statuts
Trois décrivent un payout toujours actif qui retient vos fonds. Les trois autres sont définitifs. Un payout ne quitte jamais l'un de ces derniers.
| Statut | Signification | Vos fonds |
|---|---|---|
pending | Enregistré, en attente de confirmation par un membre de votre équipe | Réservés |
review | Retenu par Wajub avant exécution pendant la vérification d'une mesure de protection | Réservés |
processing | Transmis à l'opérateur. Un payout API commence dans cet état | Réservés |
succeeded | Le destinataire a reçu les fonds et complete passe à true | Débités, frais prélevés |
failed | Les fonds ne sont pas arrivés et failure_reason indique pourquoi | Libérés, aucuns frais prélevés |
cancelled | Refusé avant toute exécution | Libérés |
Votre intégration ne peut jamais produire deux de ces six statuts. POST /transfers crée un payout
avec processing, ou avec review lorsqu'une mesure de protection le retient. pending et
cancelled appartiennent à la file de confirmation de la Konsole. Un payout reste pending tant
qu'un collègue doit l'approuver, puis passe à cancelled en cas de refus. Vous pouvez les lire avec
GET /transfers si votre équipe utilise cette file, mais jamais les obtenir en réponse à votre
propre appel.
Événement émis ou non
Il existe quatre événements de transfert. Ils ne couvrent pas les six statuts. Ces absences sont importantes.
| Statut atteint par le payout | Événement reçu |
|---|---|
processing, à la création | transfer.created, puis transfer.processing |
review | transfer.created, puis plus rien |
processing, après approbation d'une retenue | transfer.processing |
succeeded | transfer.succeeded |
failed | transfer.failed |
pending ou cancelled | Aucun événement |
transfer.cancelled n'existe pas
Un payout refusé dans la file de confirmation change de statut sans événement. La Konsole propose
exactement quatre événements de transfert lors de l'abonnement d'un endpoint. Celui-ci n'en fait
pas partie. Si votre équipe utilise la file, lisez cancelled avec GET /transfers. N'attendez
pas un événement qui n'arrivera jamais.
transfer.created contient toujours processing ou review, jamais pending. Le payload est
sérialisé après la validation de la transaction de création, pas lors de l'insertion. La ligne
pending de courte durée qui existe dans cette transaction ne vous parvient donc jamais.
Les trois séquences réellement observées
Le payout réussit
Quatre secondes, trois événements. Seul le dernier indique un mouvement de fonds.
Un payout réussi
- 14:31:02
transfer.createdstatus: processingLe payout est enregistré et votre solde est réservé. Aucun fonds n'a encore quitté le compte.
- 14:31:02
transfer.processingstatus: processingMTN MoMoTransmis à MTN MoMo. Le statut reste identique à l'événement précédent, car seul le détenteur du payout a changé, pas le payout lui-même.
- 14:31:06
transfer.succeededstatus: succeededLe destinataire a reçu les fonds. La réservation est réglée, les frais sont prélevés et trois clés apparaissent sur l’objet.
- complete
- true
- succeeded_at
- 2026-09-12T14:31:06+00:00
- provider_reference
- MP260912.1431.B84213
Les deux premiers événements ne vous apprennent rien de plus que la réponse 201. Agissez sur
transfer.succeeded et ignorez les autres, sauf si vous affichez une chronologie au support.
Le payout échoue
Les deux mêmes événements sont suivis d'un troisième différent. Un échec survenu dans Wajub avant la transmission à un opérateur et un échec renvoyé par l'opérateur sont identiques sur le réseau : même séquence, même plage de temps et même nom d'événement.
Un payout échoué
- 14:31:02
transfer.createdstatus: processingIdentique au payout réussi. Rien ici ne permet de prévoir le résultat.
- 14:31:02
transfer.processingstatus: processingMTN MoMoToujours identique. L'opérateur détient le payout et n'a pas encore répondu.
- 14:31:09
transfer.failedstatus: failedL'opérateur l'a refusé. Votre réservation est libérée, les frais ne sont jamais prélevés et deux clés propres à ce statut apparaissent.
Ces deux clés contiennent toute l'information fournie sur un échec. Lisez-les après avoir vérifié le statut au lieu de supposer leur présence.
failure_reason est un code court à utiliser dans vos branches et vos logs. failure_message est
une phrase à montrer à une personne. Transferts liste les codes écrits par
Wajub et explique pourquoi les autres viennent directement de l'opérateur. Pour un payout sur le
circuit Wajub, transfer.failed libère aussi votre réservation. Le principal et l'estimation des
frais reviennent dans available avant la fin de votre handler.
Le payout est retenu
transfer.created arrive avec status: "review", puis plus rien pendant des minutes ou des heures.
Le payout attend l'intervention d'une personne chez Wajub. Aucun événement ne marque cette attente,
car le payout a été créé directement dans cet état.
La résolution produit l'un de deux événements. transfer.processing signifie que le payout est
approuvé et suit maintenant son traitement normal. transfer.failed avec failure_reason: admin_rejected signifie qu'il est refusé.
Une retenue pour examen produit un silence, pas un retard
Un handler qui considère l'absence de transfer.processing après une minute comme une erreur
alertera quelqu'un à trois heures du matin alors que le processus fonctionne normalement. Dans
votre handler transfer.created, vérifiez data.status === 'review', informez votre équipe
chargée des opérations, puis arrêtez le chronomètre.
Structure d'un événement
Tous les événements de tous les produits utilisent la même enveloppe. Le transfert constitue
l'intégralité de data, avec la même structure que la réponse de GET /transfers/{uid}.
L'enveloppe contient sept clés. Deux d'entre elles ne correspondent pas à ce que leur nom suggère.
idstringfacultatifevt_ en live et evt_test_ dans la sandbox. Votre clé de déduplication, voir ci-dessous.eventstringfacultatifevent, pas type.dataobjectfacultatiflivemodebooleanfacultatiffalse pour un payout sandbox. Le seul champ qui distingue les deux environnements.api_versionstringfacultatifdata. Fixer une version plus ancienne change les clés présentes.pending_webhooksnumberfacultatifrequestobjectfacultatifL'une de ces sept clés mérite un avertissement. Une erreur à cet endroit ne produit aucun message.
Lisez event, pas type
Le champ qui nomme l'événement est event. Une enveloppe Wajub ne possède aucune clé type.
Utiliser event.type par habitude produit undefined, un switch qui ne correspond à rien et un
handler qui renvoie 200 sans rien faire. C'est la cause la plus fréquente d'un échec silencieux
de cette intégration.
request vous sera peu utile. request.id n'est jamais renseigné. request.idempotency_key est un
identifiant généré pour l'événement lui-même, sans rapport avec la valeur Idempotency-Key envoyée
avec POST /transfers. Pour relier un événement à votre propre appel, utilisez data.id ou placez
votre référence dans metadata lors de la création du payout. Les métadonnées reviennent dans
chaque événement.
Deux caractéristiques de data sont à connaître avant de l'utiliser. Le bénéficiaire, le moyen de
paiement et le prestataire sont toujours développés. Un événement de transfert indique donc la
destination des fonds sans second appel API. De plus, metadata ne contient pas uniquement vos
données. Sur un payout live qui utilise le circuit Wajub, Wajub ajoute un objet reservation, puis
un objet settlement lorsque le payout implique une conversion de devise. Lisez les clés que vous
avez ajoutées et ne vérifiez jamais la structure exacte de l'ensemble.
Le handler
Effectuez trois opérations dans cet ordre : prouvez que l'événement vient de Wajub, répondez, puis traitez-le. Répondre avant le traitement n'est pas une optimisation. Cette méthode empêche une base de données lente de provoquer une multiplication des nouvelles tentatives.
import express from 'express';
const app = express();
app.post('/webhooks/wajub', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = wajub.webhooks.constructEvent(
req.body,
req.headers['x-wajub-signature'],
req.headers['x-wajub-timestamp'],
process.env.WAJUB_WEBHOOK_SECRET,
);
} catch {
return res.status(400).send('invalid signature');
}
res.status(200).end();
queue.push(event);
});Le renvoi d'une réponse 400 pour une mauvaise signature est volontaire. Rejouer les mêmes octets
échouerait de la même façon, donc une nouvelle tentative n'apporterait rien. Vérification de
signature détaille l'algorithme, l'obligation
d'utiliser le corps brut et la fenêtre de cinq minutes de l'horodatage.
Le traitement doit être effectué en dehors de la requête. Il se divise d'abord selon le nom de l'événement, puis selon son statut.
async function handleTransferEvent(event) {
const transfer = event.data;
if (await alreadyProcessed(event.id)) {
return;
}
switch (event.event) {
case 'transfer.succeeded':
await markSupplierPaid(transfer.id, transfer.amount, transfer.currency);
break;
case 'transfer.failed':
await flagPayoutFailure(transfer.id, transfer.failure_reason);
break;
case 'transfer.created':
if (transfer.status === 'review') {
await notifyOpsPayoutHeld(transfer.id);
}
break;
}
await recordProcessed(event.id);
}transfer.processing est volontairement absent. Toute action à ce stade serait prématurée. Les
fonds n'ont pas bougé. Un handler qui livre une commande avec processing la livre avant même la
tentative de payout.
Les quatre causes d'erreur
Chacune de ces erreurs produit un handler qui passe la revue, fonctionne en préproduction et paie deux fois une personne en production.
| Erreur | Conséquence |
|---|---|
| Faire confiance à l'ordre d'arrivée des événements | Les livraisons sont placées en file et relancées séparément. transfer.succeeded peut arriver avant transfer.processing. Lisez l'état dans data.status, jamais dans la séquence |
| Ne pas dédupliquer | Le même événement peut être livré plusieurs fois. Stockez event.id et vérifiez-le en premier |
Renvoyer 4xx alors que vous souhaitez une nouvelle tentative | Tout code 4xx, sauf 408 et 429, arrête définitivement la livraison. Un framework qui répond 400 à un corps mal formé abandonne l'événement |
| Traiter avant de répondre | Le délai d'expiration est de 10 secondes. Un handler lent transforme un événement en cinq livraisons et vos nouvelles tentatives s'accumulent derrière votre propre latence |
Un abonnement à tous les événements en produit plus de quatre
Un endpoint abonné à * reçoit balance.updated lors du règlement de la réservation et
fee.charged lorsque Wajub prélève ses frais, en plus des quatre événements de transfert. Ce
comportement est normal et ne constitue pas un doublon. Abonnez-vous aux quatre événements
nécessaires, sauf si les autres vous sont utiles.
Délai avant de vous inquiéter
Dans la sandbox, le délai fixe est d'environ deux secondes. En live, il dépend de l'opérateur. Il est généralement de quelques secondes, mais atteint parfois plusieurs minutes.
Si le callback de l'opérateur n'arrive jamais, Wajub l'interroge. Le premier contrôle est effectué
30 secondes après la transmission du payout. Les suivants ont lieu environ toutes les minutes,
avec un intervalle croissant, soit une dizaine de contrôles sur environ quarante-cinq minutes. Un
payout résolu de cette façon produit le même événement transfer.succeeded ou transfer.failed que
les autres, mais plus tard. Votre handler ne peut pas distinguer ce cas et ne doit pas essayer.
Aucune expiration automatique
Après ces contrôles, un payout toujours processing conserve ce statut et Wajub reçoit une
alerte. Votre réservation reste bloquée. Aucun statut ne signifie « abandon » et aucune expiration
n'existe. Ne créez pas de délai qui marque le payout comme échoué de votre côté. Un payout
processing peut encore atteindre le téléphone du destinataire. Vous l'auriez alors payé tandis
que votre comptabilité indiquerait le contraire.
Lire plutôt qu'écouter
Les webhooks conviennent au payout que vous attendez. La lecture convient au payout que vous contrôlez.
https://api.wajub.com/transfers/{uid}Utilisez cet appel pour un rapprochement planifié, pour répondre au support ou pendant le
développement, avant de disposer d'une URL publique accessible par Wajub. Ne créez pas une boucle
autour d'un payout en attendant succeeded. Cet appel dépend de la limite générale de l'API, soit
120 requêtes par minute avec le plan par défaut. Une boucle consomme la capacité nécessaire à tous
les autres appels de votre intégration.
Pour un rapprochement en fin de journée, listez les payouts par période et comparez-les aux
événements reçus. Si un payout enregistré chez vous comme processing est définitif chez Wajub,
vous avez manqué une livraison. La Konsole affiche chaque
tentative effectuée pour vous joindre.
Si vous êtes une plateforme
Un payout effectué pour un compte connecté est livré deux fois : une fois aux endpoints de ce
compte et une fois aux vôtres. Votre copie contient un objet account supplémentaire qui désigne le
marchand concerné. Un seul endpoint peut ainsi servir tous vos comptes connectés. Sync
explique leur configuration.
Pages associées
- TransfertsL'objet, les statuts détaillés et les codes d'échec.
- Vérification de signatureL'algorithme, le corps brut et la fenêtre de l'horodatage.
- Nouvelles tentatives et ordreCe qui constitue un échec de livraison et le calendrier des nouvelles tentatives.
- Recevoir des webhooks en localTransférez de vrais événements vers localhost pendant la création du handler.