Aller au contenu

Catalogue des événements

Chaque événement émis par Wajub, ce qui le déclenche et ce qu'il transporte.

Konsole propose 72 types d'événements auxquels vous abonner. 68 d'entre eux atteignent réellement un endpoint, et les quatre qui ne l'atteignent pas sont nommés en bas de cette page. Presque tous arrivent dans la même enveloppe, décrite dans Webhooks ; seul data change de l'un à l'autre, et data contient toujours la ressource complète, identique à ce que renvoie l'endpoint REST de cette ressource. L'exception est le cycle de vie des connexions Sync, qui a son propre format.

Cette page est la liste complète, regroupée par sujet, avec ce qu'il vaut la peine de faire à la réception de chaque événement.

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 eventsCe 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. payment.* est 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.

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.

VerbeSignification
createdLa ressource existe désormais
updatedUn champ a changé
deletedLa ressource a été supprimée, ou anonymisée
processingTransmise à 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énementSe déclenche quandQue faire
payment.createdLe paiement existe et attend le payeurRien, enregistrez l'id
payment.processingL'opérateur a accepté la demande de débitAffichez un état en attente
payment.succeededLes fonds sont capturésLivrez la commande ici
payment.failedL'opérateur ou l'émetteur de la carte a refuséLibérez la réservation, prévenez le client
payment.cancelledVous avez appelé DELETE /payments/{id}, ou le payeur a cliqué sur Annuler sur la page hébergéeLibérez la réservation
payment.expiredLe délai du paiement s'est écoulé avant la confirmationLibérez la réservation
payment.attempt_failedUne tentative a été refusée sur-le-champ et le paiement est de nouveau pendingLaissez 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.

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 :

Ce que payment.attempt_failed ajoute au paiement
"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.

La sandbox envoie ce qu'envoie le live

Un paiement qui passe à expired envoie payment.expired, et un paiement qui passe à cancelled envoie payment.cancelled, en sandbox comme en live. Un délai dépassé en sandbox envoyait payment.failed et une annulation en sandbox rien du tout : un handler testé en sandbox manquait toutes les expirations du live.

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énementSe déclenche quand
payment.split.processingUne tranche a été envoyée à l'opérateur
payment.split.succeededCette tranche a été encaissée, et créditée sur votre solde
payment.split.failedCette tranche a été refusée

data contient le paiement complet, plus un objet split qui indique de quelle tranche il s'agit.

L'objet split ajouté au payload du paiement
"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énementSe déclenche quandQue faire
refund.createdLe remboursement a été accepté pour traitementEnregistrez-le, ne créditez pas encore
refund.processingLe prestataire l'exécute. Live uniquement : un remboursement sandbox est créé processing et ne l'envoie jamaisRien
refund.succeededL'argent a quitté votre soldeCréditez le client ici
refund.failedLe prestataire l'a rejetéLisez failure_reason, décidez manuellement
refund.cancelledWajub a arrêté le remboursement avant de l'envoyer, avec cancelled_by_wajub pour failure_reason. Envoyé une fois la version d'API 2026-10-12 devenue courante : jusque-là, le même remboursement arrive en refund.failed, avec status: failedRien n'a quitté votre solde. Remboursez de nouveau si le client attend toujours son argent
refund.updatedUn 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énementSe déclenche quandQue faire
transfer.createdLe transfert a été accepté et a débité votre solde disponibleEnregistrez l'id
transfer.processingTransmis au prestataire de payoutRien
transfer.succeededLe bénéficiaire a été payéMarquez le payout comme réglé
transfer.failedLe prestataire n'a pas pu payerLisez failure_reason, le montant revient sur votre solde
transfer.cancelledLe payout a été arrêté avant tout envoi : rejeté dans le Dashboard (merchant_rejected) ou annulé par Wajub (cancelled_by_wajub). Envoyé une fois la version d'API 2026-10-12 devenue courante : jusque-là, il arrive en transfer.failed, avec status: failedLe montant revient sur votre solde

Clients

ÉvénementSe déclenche quand
customer.createdUne fiche client a été créée, y compris implicitement par un paiement
customer.updatedUn champ du client a changé
customer.deletedDELETE /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 devient Deleted User <pseudonym>, 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énementSe déclenche quand
beneficiary.createdUne 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.updatedSes informations ont changé
beneficiary.deletedElle a été supprimée
beneficiary.receivable_recordedVous avez déclaré qu'un vendeur déjà payé vous doit de l'argent, avec POST /beneficiaries/{id}/receivables
beneficiary.receivable_recoveredUne 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.

ÉvénementSe déclenche quand
link.createdUn lien de paiement a été créé
link.updatedSon montant, son expiration ou son état a changé
link.deletedIl a été supprimé

Un paiement effectué via un lien émet payment.*, pas link.*. Les événements de lien concernent l'objet lien lui-même.

link.expired apparaît dans le sélecteur de Konsole mais n'est pas livré aux endpoints de votre compte. S'y abonner sur un endpoint de compte ne produit rien.

Factures

ÉvénementSe déclenche quand
invoice.createdUne facture a été créée, en brouillon : une seule fois, avec ses lignes et ses totaux
invoice.updatedUne 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.payment_succeededUn paiement effectué sur la page de la facture a réussi, chaque échéance comprise
invoice.paidLa facture est réglée en totalité, en ligne ou avec mark-paid
invoice.deletedElle 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énementSe déclenche quandQue faire
dispute.createdUn litige a été ouvert sur un paiementCommencez à rassembler des preuves
dispute.updatedUn champ a changé, ou un message a été ajoutéLisez data.messages
dispute.closedLe litige s'est terminé. Une perte l'envoie juste après dispute.lost, un gain ne l'envoie jamais : lisez data.status pour l'issueRien de plus, agissez sur dispute.lost
dispute.wonLa décision vous est favorableGardez les fonds
dispute.lostLa décision vous est défavorableLe montant est débité
dispute.deadline_approachingLa due_date est dans deux jours et le litige est encore open ou under_review. Envoyé une seule foisSoumettez vos preuves maintenant
dispute.deletedL'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énementSe déclenche quand
account.createdVous avez créé la connexion avec POST /accounts. Le marchand ne l'a pas encore acceptée : c'est account.connected, plus bas
account.updatedLe 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.deauthorizedVous avez mis fin à la connexion avec DELETE /accounts/{id}
account.payment_activatedVous avez passé payment_status à active avec PUT /accounts/{id}
account.payment_suspendedVous l'avez passé à suspended
account.change_requestedPUT /accounts/{id} a proposé une nouvelle tarification ou de nouvelles capacités, que le marchand doit approuver
account.commission_receivable_recordedUne commission n'a pas pu être prélevée à la source et le marchand la doit désormais. Live uniquement
account.commission_receivable_collectedCette 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énementdata.accountdata.id
account.created, account.updated, account.deauthorized, account.payment_activated, account.payment_suspendedacc_…Le même acc_… : data est le compte, tel que GET /accounts/{id} le renvoie
account.change_requestedacc_…Absent. La proposition est identifiée par change_id
account.commission_receivable_recorded, account.commission_receivable_collectedacc_…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énementSe déclenche quand
account.onboarding_startedLe marchand a ouvert votre lien d'onboarding
account.onboarding_email_verifiedIl a vérifié son adresse e-mail
account.onboarding_account_selectedIl a choisi le compte Wajub à connecter
account.onboarding_kyc_submittedIl a soumis son dossier de vérification
account.connectedIl a accepté la connexion. Elle est active à partir de maintenant
account.change_requestedVous avez proposé de nouvelles conditions depuis le Dashboard
account.change_acceptedLe marchand les a approuvées. Elles s'appliquent à partir de maintenant
account.change_declinedLe marchand les a refusées. Rien ne change
account.payment_activated, account.payment_suspended, account.deauthorizedLes 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 tableau events vide ne correspondent pas. Listez chaque type voulu.
  • Le corps est à plat, sans l'enveloppe habituelle : event, account (l'id acc_… de la connexion), sandbox, timestamp, puis les champs propres à l'événement. Il n'y a pas de data : lisez la connexion dans account, la clé qu'utilisent sous data les événements account.* de l'API. sync_uid porte le même id acc_… et reste pour les plateformes qui le lisent déjà. Ne vous fiez qu'aux champs documentés ici.
  • Les en-têtes sont différents : X-Sync-Event nomme l'événement, X-Idempotency-Key identifie la livraison, et X-Sync-Signature-V1 avec X-Sync-Timestamp portent la signature.
  • Vérifiez la signature comme pour toute autre livraison. X-Sync-Signature-V1 est v1= suivi du HMAC-SHA256 en hexadécimal de {timestamp}.{raw_body}, avec le secret de signature de l'endpoint pour clé, et X-Sync-Timestamp est cet horodatage en secondes Unix. Passez les deux à constructEvent dans 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 chaque X-Idempotency-Key traité, et ignorez une livraison dont la clé a déjà été vue.
  • X-Sync-Signature est conservé pour les intégrations existantes. C'est le HMAC-SHA256 du seul corps brut, en hexadécimal sans préfixe v1=. Il ne signe pas l'heure, donc il ne distingue pas un rejeu d'une livraison. Il sera retiré plus tard : vérifiez X-Sync-Signature-V1 à la place.
  • Les nouvelles tentatives sont courtes : trois tentatives, espacées de 30 secondes puis de 2 minutes, 10 secondes chacune. Ces livraisons n'apparaissent pas dans le journal des événements de Konsole.

Voici à quoi ressemble account.connected :

Une livraison account.connected
{
"event": "account.connected",
"sync_uid": "acc_7Yh2MpL4tRb3nP8sZcXv",
"account": "acc_7Yh2MpL4tRb3nP8sZcXv",
"sandbox": false,
"timestamp": "2026-09-16T10:32:41+00:00",
"team_name": "Boutique Akwa",
"capabilities": [
"read",
"payments"
],
"status": "active",
"payment_status": "active"
}

Solde et frais

ÉvénementSe déclenche quandPayload
balance.updatedVotre solde a bougéLe mouvement, pas les nouveaux totaux
fee.chargedWajub vous a débité des fraisLes frais
fee.receivedUne plateforme Sync a perçu ses frais sur un marchand connectéLes frais, plus account

balance.updated envoie le mouvement lui-même, c'est ce qu'il vous faut pour un registre.

Un payload balance.updated
{
"currency": "XAF",
"amount": 24250,
"direction": "credit",
"balance_type": "available",
"balance_before": 1840000,
"balance_after": 1864250,
"reason": "payment_settlement",
"sandbox": false
}

Les huit montants que renvoie GET /balance ne figurent pas dans ce payload. Appelez l'endpoint quand vous avez besoin des totaux.

Conformité

Émis pendant la vérification de votre compte, puis chaque fois que votre dossier change d'état.

ÉvénementSe déclenche quand
compliance.submittedVotre dossier de vérification a été soumis pour examen
compliance.verifiedLe compte est vérifié, les paiements live deviennent possibles
compliance.rejectedLe dossier a été refusé
compliance.changes_requestedUn analyste a besoin d'autre chose de votre part
compliance.inquiry.createdUne question précise a été posée sur votre dossier
compliance.inquiry.resolvedCette 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énementSe déclenche quand
provider.activatedUn prestataire a été activé sur votre compte
provider.deactivatedUn prestataire a été désactivé
provider.channel_activatedUn canal d'un prestataire est devenu disponible
provider.channel_deactivatedUn 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énementSe déclenche quand
webhook_endpoint.createdUn endpoint a été enregistré
webhook_endpoint.updatedSon URL, ses abonnements ou son état ont changé
webhook_endpoint.deletedIl 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 sérialisée par le même code que celui qui sert l'endpoint REST, avec trois conséquences à connaître.

Un champ sans valeur est absent ou null, selon la ressource. Un paiement ou un client le retire : un paiement sans description n'a tout simplement pas de clé description. Un transfert, une facture, un lien ou un compte l'envoie à null. Lisez chaque champ avec une valeur par défaut, et acceptez null.

Un objet vide reste un objet. Un metadata vide arrive en {}, comme l'endpoint REST le renvoie, jamais en [] : un handler qui le lit dans une map ou une struct ne casse pas sur l'objet vide.

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.

Une livraison payment.succeeded complète
{
"id": "evt_aio5DpN577tNU2vOxdmuZGhT",
"event": "payment.succeeded",
"livemode": true,
"created": "2026-01-15T10:32:41+00:00",
"api_version": "2026-10-11",
"pending_webhooks": 1,
"request": {
"id": null,
"idempotency_key": "idem_kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0"
},
"data": {
"id": "trx_CSUGajfv9xh0XQ5wu2lx",
"reference": "order-4172",
"amount": 25000,
"amount_paid": 25000,
"currency": "XAF",
"status": "succeeded",
"channel": "cm.mtn",
"payment_method": {
"channel": "cm.mtn",
"account": "+237670000000"
},
"customer": {
"id": "cus_4tRb3nP8sZcXvK2mQ9wL",
"name": "Amina Nkem",
"email": "[email protected]",
"phone": "+237670000000"
},
"sandbox": false,
"credited_at": "2026-01-15T10:32:41+00:00",
"created_at": "2026-01-15T10:30:00+00:00",
"updated_at": "2026-01-15T10:32:41+00:00"
}
}

Les événements qu'on cherche et qui n'existent pas

Vous attendiezCe qui existe réellement
payment.refundedrefund.succeeded
payment.partially_refundedrefund.succeeded, comparez data.amount au paiement
transfer.reviewtransfer.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.completedpayment.succeeded
account.deletedaccount.deauthorized

Les événements que Wajub enregistre pour son propre usage ne sont jamais livrés, même aux abonnés à *.

Types auxquels vous pouvez vous abonner mais qui n'arrivent jamais

Konsole vous laisse cocher ce type. Rien n'est jamais livré.

TypePourquoi
link.expiredNon livré aux endpoints de compte

payment.checkout_page_opened et payment.redirected_to_callback étaient proposés aussi, et jamais livrés non plus : ce sont des entrées de la chronologie du checkout, enregistrées dans le seul journal des événements de Konsole. Ils ne sont plus proposés. Un endpoint qui en liste déjà un le garde, sans effet, et peut toujours être enregistré avec. Les deux restent visibles dans Événements : vous pouvez toujours auditer un checkout après coup.

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 :

  • livemode vaut false en sandbox et true en live.
  • Les ids d'événement sont préfixés evt_test_ en sandbox et evt_ 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.

Ce que la copie de la plateforme ajoute à data
"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.

Que pensez-vous de ce contenu ?