Catalogue des événements
Chaque événement émis par Wajub, ce qui le déclenche et ce qu'il transporte.
Chaque événement envoyé par Wajub, regroupé par sujet, avec ce qu'il faut faire à sa réception. Tous
arrivent dans l'enveloppe des webhooks avec la ressource complète dans
data, sauf les livraisons du cycle de vie Sync, qui ont leur propre format.
Comment un endpoint décide de se déclencher
La correspondance des abonnements est une simple comparaison de chaînes avec le tableau events
que vous avez enregistré. Trois cas, pas un de plus.
Votre tableau events | Ce que vous recevez |
|---|---|
["payment.succeeded", "refund.failed"] | Exactement ces deux types |
["*"] | Tous les événements de paiement, remboursement, transfert, client, litige, facture, lien, compte, solde, frais, endpoint, conformité et prestataire, sauf les livraisons du cycle de vie Sync |
[] | La même chose que ["*"] |
Les jokers de préfixe n'existent pas. À partir de la version 2026-10-12 de l'API, enregistrer
payment.*, ou tout nom qui n'est pas un type d'événement, répond 422 ; sur les versions
antérieures, il est enregistré comme une chaîne littérale à laquelle aucun événement n'est jamais
égal. Si vous voulez tout ce qui concerne une ressource, listez ses types, copiés depuis cette page.
`*` ne couvre pas les livraisons du cycle de vie Sync
Les livraisons du cycle de vie Sync ne font correspondre les
abonnements que sur le nom exact : un endpoint enregistré avec ["*"], ou avec une liste vide, n'en
reçoit aucune. Listez chaque type voulu. Les événements de conformité et de prestataires atteignent
bien * et une liste vide.
Comment les noms sont construits
resource.verb, le verbe au passé, le tout en minuscules. Les verbes sont les mêmes d'une ressource
à l'autre, ce qui rend la liste plus courte à retenir qu'il n'y paraît.
| Verbe | Signification |
|---|---|
created | La ressource existe désormais |
updated | Un champ a changé |
deleted | La ressource a été supprimée, ou anonymisée |
processing | Transmise à un prestataire, résultat inconnu |
succeeded | État terminal, l'opération a réussi |
failed | État terminal, l'opération a échoué |
cancelled | État terminal, arrêtée volontairement |
expired | État terminal, le délai est écoulé |
Trois orthographes font perdre un après-midi : c'est succeeded, jamais completed ni complete ;
c'est cancelled avec deux l ; et Sync utilise deauthorized avec un z alors que tout le reste
de la plateforme suit l'orthographe britannique.
Paiements
Les six états que traverse un paiement, et la tentative qui échoue sans y mettre fin. Seul
payment.succeeded signifie que l'argent est là.
| Événement | Se déclenche quand | Que faire |
|---|---|---|
payment.created | Le paiement existe et attend le payeur | Rien, enregistrez l'id |
payment.processing | L'opérateur a accepté la demande de débit | Affichez un état en attente |
payment.succeeded | Les fonds sont capturés | Livrez la commande ici |
payment.failed | L'opérateur ou l'émetteur de la carte a refusé | Libérez la réservation, prévenez le client |
payment.cancelled | Vous avez appelé DELETE /payments/{id}, ou le payeur a cliqué sur Annuler sur la page hébergée | Libérez la réservation |
payment.expired | Le délai du paiement s'est écoulé avant la confirmation | Libérez la réservation |
payment.attempt_failed | Une tentative a été refusée sur-le-champ et le paiement est de nouveau pending | Laissez le payeur réessayer. Rien n'est terminé |
payment.failed transporte failure_reason dans data. Le champ est absent, et non null, quand il
n'y a aucun motif à donner.
Il n'existe pas de `payment.refunded`
Un remboursement ne produit jamais d'événement de paiement. Il produit des événements refund.*
sur l'objet remboursement. Le status du paiement lui-même affiche refunded ou partially_refunded la prochaine fois que vous le lisez, mais
aucun événement de paiement n'est émis pour ce changement.
Une tentative refusée sur-le-champ
payment.attempt_failed n'est pas une fin. Un fournisseur a refusé une tentative sur-le-champ, ou
aucun fournisseur ne pouvait la prendre, et le paiement est revenu à pending : le payeur peut
réessayer sur le même paiement. payment.processing vous est peut-être déjà parvenu pour cette
tentative, et c'est ce qui le suit. Un refus qui arrive plus tard, de l'opérateur, met fin au
paiement avec payment.failed.
data est le paiement, avec en plus un objet attempt :
"attempt": {
"failure_code": "insufficient_funds",
"failure_message": "Insufficient funds.",
"channel": "cm.mtn"
}failure_code est le code de refus du fournisseur quand il en donne un, no_eligible_provider
quand aucun fournisseur ne pouvait prendre la tentative, et payment_declined sinon. En sandbox, un
numéro qui se termine par 000001 l'envoie. Sur un canal qui demande
un code que le payeur génère, il est aussi envoyé
quand ce code manque (otp_required) ou est refusé (invalid_otp, et otp_attempts_exceeded la
troisième fois) ; les autres refus gardent le code du prestataire. Un refus de ce code par
l'opérateur arrivé plus tard envoie aussi payment.attempt_failed, pas payment.failed.
Paiements fractionnés
Émis quand un même paiement est débité en plusieurs tranches, via split_count ou split_amounts
sur POST /payments/{id}/splits.
| Événement | Se déclenche quand |
|---|---|
payment.split.processing | Une tranche a été envoyée à l'opérateur |
payment.split.succeeded | Cette tranche a été encaissée, et créditée sur votre solde |
payment.split.failed | Cette tranche a été refusée |
data contient le paiement complet, plus un objet split qui indique de quelle tranche il s'agit.
"split": {
"id": "splt_9mWvL2xR7tB5nY4hC6dF",
"split_number": 2,
"amount": 500000,
"currency": "XAF",
"split_currency": "XAF",
"status": "succeeded",
"reference": "order-4172-2",
"provider_reference": "MP260115.1032.B41209",
"initiated_at": "2026-01-15T10:32:04+00:00",
"succeeded_at": "2026-01-15T10:32:41+00:00"
}Le paiement parent n'atteint payment.succeeded qu'une fois toutes les tranches réussies, ou quand
vous le clôturez avec POST /payments/{id}/close. Compter
vous-même les payment.split.succeeded pour décider que la commande est complète, c'est le meilleur
moyen de livrer une commande à moitié payée.
Le live les envoie pour les mêmes appels, sur les canaux Mobile Money. Le paiement parent envoie
aussi payment.processing chaque fois qu'une tranche part chez le payeur, et il est partial entre
deux tranches. Ni le live ni la sandbox ne fractionnent un paiement d'eux-mêmes : un débit unique
au-dessus du plafond par transaction indiqué dans Plafonds et quotas est refusé
avec un 422, et POST /payments/{id}/splits le règle en tranches.
Paiements fractionnés décrit ce que renvoie chaque appel.
Remboursements
| Événement | Se déclenche quand | Que faire |
|---|---|---|
refund.created | Le remboursement a été accepté pour traitement | Enregistrez-le, ne créditez pas encore |
refund.processing | Le prestataire l'exécute. Live uniquement : un remboursement sandbox est créé processing et ne l'envoie jamais | Rien |
refund.succeeded | L'argent a quitté votre solde | Créditez le client ici |
refund.failed | Le prestataire l'a rejeté | Lisez failure_reason, décidez manuellement |
refund.cancelled | Wajub a arrêté le remboursement avant de l'envoyer. Il arrive en refund.failed, avec status: failed et cancelled_by_wajub pour failure_reason | Rien n'a quitté votre solde. Remboursez de nouveau si le client attend toujours son argent |
refund.updated | Un remboursement live a changé de voie : qui l'envoie (execution_mode) ou pourquoi (manual_reason) | Prévenez votre équipe quand il indique merchant : c'est à vous de l'envoyer |
data.transaction est l'id du paiement remboursé, sous forme de chaîne, pas d'objet imbriqué.
Transferts
Les payouts depuis votre solde, que ce soit vers un portefeuille Mobile Money, un compte bancaire ou une carte.
| Événement | Se déclenche quand | Que faire |
|---|---|---|
transfer.created | Le transfert a été accepté et a débité votre solde disponible | Enregistrez l'id |
transfer.processing | Transmis au prestataire de payout | Rien |
transfer.succeeded | Le bénéficiaire a été payé | Marquez le payout comme réglé |
transfer.failed | Le prestataire n'a pas pu payer | Lisez failure_reason, le montant revient sur votre solde |
transfer.cancelled | Le payout a été arrêté avant tout envoi : rejeté dans le Dashboard ou annulé par Wajub. Il arrive en transfer.failed, avec status: failed et merchant_rejected ou cancelled_by_wajub pour failure_reason | Le montant revient sur votre solde |
Un transfert en revue envoie transfer.created, puis attend
review est un vrai statut de transfert : Wajub peut retenir un payout pour examen avant de
l'envoyer. Il envoie transfer.created, avec status: review, et rien de plus tant
qu'il attend. Approuvé, il envoie transfer.processing une seule fois, puis transfer.succeeded
ou transfer.failed. Refusé, il envoie transfer.failed avec admin_rejected. L'un comme
l'autre peut arriver des heures plus tard. Construisez l'état d'attente sur le statut, pas sur le
silence.
Clients
| Événement | Se déclenche quand |
|---|---|
customer.created | Une fiche client a été créée, y compris implicitement par un paiement |
customer.updated | Un champ du client a changé |
customer.deleted | DELETE /customers/{id} a été exécuté, et la fiche a été anonymisée |
Le payload de customer.deleted est le client après anonymisation : le nom est remplacé,
et l'e-mail, le téléphone et la date de naissance ont disparu. Si vous
avez besoin des valeurs d'origine pour vos propres données, vous devez les avoir conservées.
Bénéficiaires
| Événement | Se déclenche quand |
|---|---|
beneficiary.created | Une destination de payout a été enregistrée : par POST /beneficiaries, à la volée dans un POST /transfers, ou comme compte de retrait dans le Dashboard |
beneficiary.updated | Ses informations ont changé |
beneficiary.deleted | Elle a été supprimée |
beneficiary.receivable_recorded | Vous avez déclaré qu'un vendeur déjà payé vous doit de l'argent, avec POST /beneficiaries/{id}/receivables |
beneficiary.receivable_recovered | Une ligne de payout groupé a repris tout ou partie de cette dette |
data contient, pour ces deux événements, la dette elle-même : id, beneficiary, reference,
refund, currency, amount, recovered, outstanding, status, settled_at et created_at,
montants en unité principale. Voir l'endpoint des créances.
Liens de paiement
| Événement | Se déclenche quand |
|---|---|
link.created | Un lien de paiement a été créé |
link.updated | Son montant, son expiration ou son état a changé |
link.deleted | Il a été supprimé |
link.expired | Son expiry_date est passée : envoyé une fois, dans la journée, sauf si le lien a été archivé entre-temps |
Un paiement effectué via un lien émet payment.*, pas link.*. Les événements de lien concernent
l'objet lien lui-même. Les endpoints propres à un lien, réglés dans son onglet Webhooks, reçoivent
aussi ses paiements, billets, dons, commandes et remboursements : voir
Les webhooks d'un lien.
Factures
| Événement | Se déclenche quand |
|---|---|
invoice.created | Une facture a été créée, en brouillon : une seule fois, avec ses lignes et ses totaux |
invoice.updated | Une ligne, un statut ou une date d'échéance a changé : une seule fois par PUT /invoices/{id}, avec les lignes et les totaux, et pas du tout pour un PUT qui ne change rien |
invoice.sent | La facture a été envoyée : son statut est devenu sent |
invoice.payment_succeeded | Un paiement effectué sur la page de la facture a réussi, chaque échéance comprise |
invoice.paid | La facture est réglée en totalité, en ligne ou avec mark-paid |
invoice.cancelled | Elle a été annulée |
invoice.deleted | Elle a été supprimée |
Le paiement et la facture se désignent l'un l'autre
Quand un client paie une facture, payment.succeeded se déclenche pour le paiement, puis
invoice.updated et invoice.payment_succeeded pour la facture, et invoice.paid une fois
celle-ci réglée. Livrez à partir de payment.succeeded : son data.invoice est l'id de la
facture payée, et les événements de facture portent la facture elle-même.
Litiges
Rétrofacturations et réclamations. Ce sont les événements assortis des délais les plus courts.
| Événement | Se déclenche quand | Que faire |
|---|---|---|
dispute.created | Un litige a été ouvert sur un paiement | Commencez à rassembler des preuves |
dispute.updated | Un champ a changé, ou un message a été ajouté | Lisez data.messages |
dispute.closed | Le litige s'est terminé. Une perte l'envoie juste après dispute.lost, un gain ne l'envoie jamais : lisez data.status pour l'issue | Rien de plus, agissez sur dispute.lost |
dispute.won | La décision vous est favorable | Gardez les fonds |
dispute.lost | La décision vous est défavorable | Le montant est débité |
dispute.deadline_approaching | La due_date est dans deux jours et le litige est encore open ou under_review. Envoyé une seule fois | Soumettez vos preuves maintenant |
dispute.deleted | L'enregistrement du litige a été supprimé | Rien |
Comptes, pour les plateformes Sync
Ces événements décrivent un marchand connecté à une plateforme Sync, pas votre propre compte. Ceux qu'envoie l'API arrivent dans l'enveloppe habituelle :
| Événement | Se déclenche quand |
|---|---|
account.created | Vous avez créé la connexion avec POST /accounts. Le marchand ne l'a pas encore acceptée : c'est account.connected, plus bas |
account.updated | Le compte a changé, et data est le compte tel que ce changement l'a laissé. Envoyé quand PUT /accounts/{id} modifie la connexion (son email, ses metadata, son callback, son payment_status, ses conditions de versement, ou les capacités et la tarification d'une connexion pas encore acceptée), quand POST /accounts/{id}/token remet une invitation annulée en pending, et quand Wajub envoie le versement d'un marchand Lite, ce qui avance last_payout_at. Un appel qui ne change rien n'envoie rien. Ni une proposition seule, qui est account.change_requested, ni DELETE, qui est account.deauthorized, ne l'envoient. Un changement de payment_status envoie aussi account.payment_activated ou account.payment_suspended |
account.deauthorized | Vous avez mis fin à la connexion avec DELETE /accounts/{id} |
account.payment_activated | Vous avez passé payment_status à active avec PUT /accounts/{id} |
account.payment_suspended | Vous l'avez passé à suspended |
account.change_requested | PUT /accounts/{id} a proposé une nouvelle tarification ou de nouvelles capacités, que le marchand doit approuver |
account.commission_receivable_recorded | Une commission n'a pas pu être prélevée à la source et le marchand la doit désormais. Live uniquement |
account.commission_receivable_collected | Cette commission a été prélevée sur le solde Wajub du marchand |
Chacun d'eux désigne sa connexion dans data.account : l'identifiant acc_…, la valeur que vous
envoyez dans X-Sync. Lisez la connexion à cet endroit, pas dans data.id, qui n'est pas toujours
la connexion :
| Événement | data.account | data.id |
|---|---|---|
account.created, account.updated, account.deauthorized, account.payment_activated, account.payment_suspended | acc_… | Le même acc_… : data est le compte, tel que GET /accounts/{id} le renvoie |
account.change_requested | acc_… | Absent. La proposition est identifiée par change_id |
account.commission_receivable_recorded, account.commission_receivable_collected | acc_… | crv_…, la commission due |
Sur ces événements, account est l'identifiant lui-même, une chaîne. Sur la copie d'un événement
de paiement, de remboursement, de transfert ou de litige que reçoit une plateforme, et sur fee.received,
account est un objet et l'identifiant se trouve dans account.id : voir
Les plateformes Sync reçoivent une copie.
Il n'existe pas de account.deleted. account.created annonce l'invitation que vous avez créée ;
l'acceptation du marchand est account.connected, plus bas. Quand le marchand révoque lui-même la
connexion, le account.deauthorized que vous recevez est celui au format Sync.
Le reste de la vie de la connexion, ainsi que les mêmes changements quand ils sont faits depuis le Dashboard plutôt que par l'API, sont envoyés au format Sync décrit dans la section suivante :
| Événement | Se déclenche quand |
|---|---|
account.onboarding_started | Le marchand a ouvert votre lien d'onboarding |
account.onboarding_email_verified | Il a vérifié son adresse e-mail |
account.onboarding_account_selected | Il a choisi le compte Wajub à connecter |
account.onboarding_kyc_submitted | Il a soumis son dossier de vérification |
account.onboarding_declined | Il a refusé la connexion. Rien n'est connecté et votre lien d'invitation fonctionne toujours. reason contient ses mots quand il en a donné |
account.connected | Il a accepté la connexion. Elle est active à partir de maintenant |
account.change_requested | Vous avez proposé de nouvelles conditions depuis le Dashboard |
account.change_accepted | Le marchand les a approuvées. Elles s'appliquent à partir de maintenant |
account.change_declined | Le marchand les a refusées. Rien ne change |
account.payment_activated, account.payment_suspended, account.deauthorized | Les mêmes changements, faits depuis le Dashboard ou déclenchés par la vérification du marchand |
Livraisons du cycle de vie Sync
Les événements du second tableau ne voyagent pas comme les autres. Ils sont envoyés aux endpoints de la plateforme propriétaire de la connexion, dans l'environnement de cette connexion.
- Seul un abonnement exact les reçoit.
*et un tableaueventsvide ne correspondent pas. Listez chaque type voulu. - Le corps est à plat, sans l'enveloppe habituelle :
event,account(l'idacc_…de la connexion),sandbox,timestamp, puis les champs propres à l'événement. Il n'y a pas dedata: lisez la connexion dansaccount, la clé qu'utilisent sousdatales événementsaccount.*de l'API. Ne vous fiez qu'aux champs documentés ici. - Les en-têtes sont différents :
X-Sync-Eventnomme l'événement,X-Idempotency-Keyidentifie la livraison, etX-Sync-Signature-V1avecX-Sync-Timestampportent la signature. - Vérifiez la signature comme pour toute autre livraison.
X-Sync-Signature-V1estv1=suivi du HMAC-SHA256 en hexadécimal de{timestamp}.{raw_body}, avec le secret de signature de l'endpoint pour clé, etX-Sync-Timestampest cet horodatage en secondes Unix. Passez les deux àconstructEventdans les SDK, ou vérifiez-les à la main comme dans Vérification de signature, et refusez une livraison dont l'horodatage s'écarte de plus de 300 secondes de votre horloge. Enregistrez ensuite chaqueX-Idempotency-Keytraité, et ignorez une livraison dont la clé a déjà été vue. - Seul un
2xxvaut livraison, comme pour tout autre événement : une redirection n'est pas suivie et compte comme un échec. Chaque tentative part vers l'endpoint tel qu'il est au moment de l'envoi, signée avec son secret actuel, et un endpoint désactivé ou supprimé entre-temps ne reçoit plus rien. - Les nouvelles livraisons ne durent que quelques minutes : gardez l'endpoint disponible et répondez en moins de 10 secondes. Ces livraisons n'apparaissent pas dans le journal des événements de Konsole.
Voici à quoi ressemble account.connected :
Solde et frais
| Événement | Se déclenche quand | Payload |
|---|---|---|
balance.updated | Votre solde a bougé | Le mouvement, pas les nouveaux totaux |
fee.charged | Wajub vous a débité des frais | Les frais |
fee.received | Une plateforme Sync a perçu ses frais sur un marchand connecté | Les frais, plus account |
Un débit de frais n'est pas un `balance.updated`
Les débits de frais sont volontairement exclus de balance.updated et remontés à la place sous
forme de fee.charged. Si vous additionnez les mouvements balance.updated pour tenir un registre
local, vous dépasserez exactement du montant des frais. Abonnez-vous aux deux.
balance.updated envoie le mouvement lui-même, c'est ce qu'il vous faut pour un registre.
Les totaux que renvoie GET /balance ne figurent pas dans ce payload. Appelez l'endpoint quand
vous en avez besoin.
Conformité
Émis pendant la vérification de votre compte, puis chaque fois que votre dossier change d'état.
| Événement | Se déclenche quand |
|---|---|
compliance.submitted | Votre dossier de vérification a été soumis pour examen |
compliance.verified | Le compte est vérifié, les paiements live deviennent possibles |
compliance.rejected | Le dossier a été refusé |
compliance.changes_requested | Un analyste a besoin d'autre chose de votre part |
compliance.inquiry.created | Une question précise a été posée sur votre dossier |
compliance.inquiry.resolved | Cette question a été clôturée |
Pour une plateforme qui inscrit des marchands, compliance.verified est le signal qu'un compte
connecté peut commencer à accepter des paiements live. Ne le déduisez de rien d'autre.
Prestataires
Émis quand l'ensemble des prestataires et des canaux disponibles sur votre compte change.
| Événement | Se déclenche quand |
|---|---|
provider.activated | Un prestataire a été activé sur votre compte |
provider.deactivated | Un prestataire a été désactivé |
provider.channel_activated | Un canal d'un prestataire est devenu disponible |
provider.channel_deactivated | Un canal a été retiré |
Utile si vous mettez en cache les canaux de votre compte au lieu d'appeler GET /providers à chaque
checkout.
Endpoints de webhook
| Événement | Se déclenche quand |
|---|---|
webhook_endpoint.created | Un endpoint a été enregistré |
webhook_endpoint.updated | Son URL, ses abonnements ou son état ont changé |
webhook_endpoint.deleted | Il a été supprimé |
Utile sur un compte plateforme où plusieurs personnes peuvent modifier la configuration. Notez que l'endpoint qui reçoit cet événement peut être celui qui vient d'être modifié.
Ce que contient réellement data
data est la ressource telle que la renvoie son endpoint REST. Trois points sont à connaître.
Un champ sans valeur peut être absent ou null. Lisez chaque champ avec une valeur par défaut,
et acceptez null.
Un metadata vide arrive en {}, jamais en [].
Le payload est toujours dans la dernière version de l'API. La version fixée s'applique à
vos appels API, pas aux livraisons de webhooks : l'api_version de l'enveloppe est la version
courante de la plateforme au moment où l'événement a été enregistré, et le corps est structuré pour
cette version. Épingler votre compte sur 2026-08-01 ne change pas ce que reçoit votre endpoint.
Les événements qu'on cherche et qui n'existent pas
| Vous attendiez | Ce qui existe réellement |
|---|---|
payment.refunded | refund.succeeded |
payment.partially_refunded | refund.succeeded, comparez data.amount au paiement |
transfer.review | transfer.created, avec status: review. Rien d'autre jusqu'à la décision |
payout.* | transfer.*, les payouts sont des transferts dans l'API |
subscription.* | invoice.*, la facturation récurrente émet des événements de facture |
checkout.session.completed | payment.succeeded |
account.deleted | account.deauthorized |
Sandbox et live sont deux flux séparés
Un endpoint appartient à un seul environnement. Un endpoint sandbox ne reçoit que des événements sandbox, et un endpoint live que des événements live. Deux choses changent dans le payload :
livemodevautfalseen sandbox ettrueen live.- Les ids d'événement sont préfixés
evt_test_en sandbox etevt_en live.
Un handler qui code en dur la longueur d'un id ou qui retire un préfixe cassera dès qu'il rencontrera l'autre environnement.
Les plateformes Sync reçoivent une copie
Quand un paiement, un remboursement ou un transfert appartient à un marchand connecté via Sync, ou
qu'un litige est ouvert sur un tel paiement, l'événement est livré deux fois : une fois aux endpoints
du marchand, et une fois à ceux de la plateforme, sous forme d'événement distinct avec son propre
id. La copie de la plateforme porte un objet account supplémentaire pour que vous sachiez de quel
marchand connecté elle provient. Les liens, factures et bénéficiaires créés par la connexion ne sont
pas copiés : GET /events avec X-Sync liste leurs événements.
"account": {
"id": "acc_7Yh2MpL4tRb3nP8sZcXv",
"reference": "boutique-akwa"
}Dédupliquez sur l'id de l'événement, pas sur l'id du paiement : les deux copies décrivent le même paiement et portent volontairement deux ids d'événement différents.
Pages associées
- WebhooksL'enveloppe, les en-têtes et les trois règles.
- Nouvelles tentatives et ordrePourquoi le même événement arrive deux fois, et sur quoi vous appuyer.
- Cycle de vie d'un paiementQuels changements d'état émettent un événement, et lesquels non.
- Déclencher un événementProduisez huit de ces événements à la demande depuis votre terminal.