Event catalogue
Every event Wajub emits, what triggers it, and what arrives with it.
Konsole offers 60 event types to subscribe to. 57 of them actually reach an endpoint, and
the three that do not are named at the bottom of this page. They all arrive in the same envelope,
described in Webhooks; only data changes from one to the next, and
data is always the full resource, identical to what the REST endpoint for that resource returns.
This page is the complete list, grouped by what the event is about, with the thing worth doing when it lands.
How an endpoint decides to fire
Subscription matching is a plain string comparison against the events array you registered.
Three cases, and no fourth.
Your events array | What you receive |
|---|---|
["payment.succeeded", "refund.failed"] | Exactly those two types |
["*"] | Every payment, refund, transfer, customer, dispute, invoice, link, account, balance, fee and endpoint event |
[] | The same as ["*"] |
A typo in the API is accepted and never fires
Konsole checks what you tick against a list of valid names. POST /webhooks does not: it
validates events as a non-empty array and nothing more. payment.succeded,
payment.SUCCEEDED and payment.* are all stored happily, match nothing, and your endpoint
stays silent forever. Copy the names from this page.
Prefix wildcards do not exist. payment.* is a literal string that no event is ever equal to. If
you want everything under one resource, list its types.
`*` does not cover compliance and provider events
Those two families are emitted by a different part of the platform, and it matches subscriptions
by exact name only. An endpoint registered with ["*"] receives everything else and none of
them. List compliance.* and provider.* types explicitly if you want them.
How the names are built
resource.verb, verb in the past tense, both lowercase. The verbs are stable across resources,
which makes the list shorter to remember than it looks.
| Verb | Meaning |
|---|---|
created | The resource now exists |
updated | A field changed |
deleted | The resource was removed, or anonymised |
processing | Handed to a provider, outcome unknown |
succeeded | Terminal, it worked |
failed | Terminal, it did not |
cancelled | Terminal, stopped on purpose |
expired | Terminal, the window closed |
Three spellings cost people an afternoon: it is succeeded, never completed or complete; it
is cancelled with two l; and Sync uses deauthorized with a z while everything else in the
platform is British spelling.
Payments
The six states a payment moves through. Only payment.succeeded means money.
| Event | Fires when | What to do |
|---|---|---|
payment.created | The payment exists and is waiting for the payer | Nothing, record the id |
payment.processing | The operator accepted the debit request | Show a pending state |
payment.succeeded | The funds are captured | Fulfil the order here |
payment.failed | The operator or the card issuer declined | Release the reservation, tell the customer |
payment.cancelled | The payer refused, or you called DELETE /payments/{id} | Release the reservation |
payment.expired | The payment window closed before confirmation | Release the reservation |
payment.failed carries failure_reason in data. The field is absent, not null, when there is
no reason to give.
There is no `payment.refunded`
A refund never produces a payment event. It produces refund.* events on the refund object. The
payment's own status becomes refunded or partially_refunded in the database, and you will
see that value the next time you read the payment, but no payment event is emitted for it.
Payment splits
Emitted when a single payment is debited in several instalments, using split_count or
split_amounts on POST /payments.
| Event | Fires when |
|---|---|
payment.split.processing | One instalment was sent to the operator |
payment.split.succeeded | That instalment cleared |
payment.split.failed | That instalment was declined |
data is the full payment, plus a split object naming which instalment this was.
"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"
}The parent payment reaches payment.succeeded only once every instalment has succeeded. Counting
payment.split.succeeded yourself to decide the order is complete is a way to ship an order that
is half paid.
Split execution is sandbox only today
In sandbox, split_count and split_amounts are honoured and these three events fire. In live,
a mobile money payment above the per-transaction cap is refused with a 422 whose message asks
you to split it, and the split is not executed. Until that lands, keep live charges under the cap
listed in Limits.
Refunds
| Event | Fires when | What to do |
|---|---|---|
refund.created | The refund was accepted for processing | Record it, do not credit yet |
refund.processing | The provider is executing it | Nothing |
refund.succeeded | The money left your balance | Credit the customer here |
refund.failed | The provider rejected it | Read failure_reason, decide manually |
refund.cancelled | The refund was stopped before execution | Nothing |
data.transaction is the id of the payment being refunded, as a string, not a nested object.
Transfers
Payouts out of your balance, whether to a mobile money wallet, a bank account or a card.
| Event | Fires when | What to do |
|---|---|---|
transfer.created | The transfer was accepted and debited your available balance | Record the id |
transfer.processing | Handed to the payout provider | Nothing |
transfer.succeeded | The beneficiary was paid | Mark the payout settled |
transfer.failed | The provider could not pay | Read failure_reason, the amount returns to your balance |
A transfer under review emits nothing
review is a real transfer status: a payout above 1,000,000 XAF, your first live payout, or one
more than five times your own average gets parked for a human decision. No event is emitted when
it enters review. You learn the outcome from transfer.succeeded or transfer.failed, which can
arrive hours later, or by reading the transfer. Build the waiting state on the status, not on the
silence.
Customers
| Event | Fires when |
|---|---|
customer.created | A customer record was created, including implicitly by a payment |
customer.updated | Any field on the customer changed |
customer.deleted | DELETE /customers/{id} ran, and the record was anonymised |
The payload of customer.deleted is the customer after anonymisation: the name reads
Deleted User <pseudonym>, and the email, phone and date of birth are gone. If you need the
original values for your own records, you must have kept them.
Beneficiaries
| Event | Fires when |
|---|---|
beneficiary.created | A payout destination was saved |
beneficiary.updated | Its details changed |
beneficiary.deleted | It was removed |
Payment links
| Event | Fires when |
|---|---|
link.created | A payment link was created |
link.updated | Its amount, expiry or state changed |
link.deleted | It was deleted |
A payment made through a link emits payment.*, not link.*. The link events are about the link
object itself.
link.expired appears in the Konsole picker but belongs to the per link webhook system, not to
your account endpoints. Subscribing to it on an account endpoint produces nothing.
Invoices
| Event | Fires when |
|---|---|
invoice.created | An invoice was issued |
invoice.updated | A line, a status or a due date changed |
invoice.deleted | It was deleted |
There is no `invoice.paid`
When an invoice is settled, what fires is payment.succeeded for the payment that settled it,
followed by invoice.updated when the invoice status changes. Reconcile on the payment, and use
the invoice reference you put in the payment metadata to connect the two.
Disputes
Chargebacks and complaints. These are the events with the shortest deadlines attached to them.
| Event | Fires when | What to do |
|---|---|---|
dispute.created | A dispute was opened against a payment | Start gathering evidence |
dispute.updated | A field changed, or a message was added | Read data.messages |
dispute.closed | The dispute ended without a winner being recorded | Nothing |
dispute.won | The decision went your way | Keep the funds |
dispute.lost | The decision went against you | The amount is debited |
dispute.deleted | The dispute record was removed | Nothing |
Accounts, for Sync platforms
These describe a connected merchant on a Sync platform, not your own account.
| Event | Fires when |
|---|---|
account.created | A merchant connected to your platform |
account.updated | Their details or pricing changed |
account.deauthorized | The connection ended, by deletion or by the merchant revoking it |
account.payment_activated | The merchant can now take live payments |
account.payment_suspended | Their ability to take payments was suspended |
account.deauthorized fires for both deletion and revocation. There is no separate
account.deleted.
Balance and fees
| Event | Fires when | Payload |
|---|---|---|
balance.updated | Your balance moved | The movement, not the new totals |
fee.charged | Wajub debited a fee from you | The fee |
fee.received | A Sync platform collected its fee on a connected merchant | The fee, plus account |
A fee debit is not a `balance.updated`
Fee debits are deliberately excluded from balance.updated and surfaced as fee.charged
instead. If you sum balance.updated movements to keep a local ledger, you will be over by
exactly the fees. Subscribe to both.
balance.updated sends the movement itself, which is what you want for a ledger.
The eight figures GET /balance returns are not in this payload. Call the endpoint when you need
the totals.
Compliance
Emitted while your account is being verified, and whenever your file changes state afterwards.
| Event | Fires when |
|---|---|
compliance.submitted | Your verification file was submitted for review |
compliance.verified | The account is verified, live money becomes possible |
compliance.rejected | The file was refused |
compliance.changes_requested | A reviewer needs something else from you |
compliance.inquiry.created | A specific question was raised on your file |
compliance.inquiry.resolved | That question was closed |
For a platform onboarding merchants, compliance.verified is the signal that a connected account
may start taking live payments. Do not infer it from anything else.
Providers
Emitted when the set of rails available to your account changes.
| Event | Fires when |
|---|---|
provider.activated | A provider was enabled on your account |
provider.deactivated | A provider was disabled |
provider.channel_activated | One channel of a provider became available |
provider.channel_deactivated | One channel was withdrawn |
Worth subscribing to if you cache the channel list rather than calling GET /channels on every
checkout.
Webhook endpoints
| Event | Fires when |
|---|---|
webhook_endpoint.created | An endpoint was registered |
webhook_endpoint.updated | Its URL, subscriptions or state changed |
webhook_endpoint.deleted | It was removed |
Useful on a platform account where several people can change the configuration. Note that the endpoint receiving this event may be the one that was just changed.
What data actually contains
data is the resource serialised by the same code that serves the REST endpoint, with two
consequences worth knowing.
Null fields are removed, not sent as null. A payment with no description has no description
key at all. Read with a default, never assume the key exists.
The payload is always in the latest API version. Version pinning applies to your API calls, not
to webhook deliveries: the envelope's api_version is the platform's current version at the moment
the event was recorded, and the body is shaped for that version. Pinning your account to
2026-08-01 does not change what your endpoint receives.
`request.idempotency_key` is not your key
Events carry an idem_… value generated for the event itself, and request.id is null. Neither
field reflects the Idempotency-Key header you sent when you created the payment. To connect an
event to your own order, use data.reference, which is the reference you supplied, or
data.metadata.
Events people look for and do not find
| You expected | What actually exists |
|---|---|
payment.refunded | refund.succeeded |
payment.partially_refunded | refund.succeeded, compare data.amount to the payment |
invoice.paid | payment.succeeded, then invoice.updated |
transfer.review | Nothing. Read the transfer status |
payout.* | transfer.*, payouts are transfers in the API |
subscription.* | invoice.*, recurring billing emits invoice events |
checkout.session.completed | payment.succeeded |
account.deleted | account.deauthorized |
transaction.* | Internal checkout telemetry. Never delivered, even to * subscribers |
Three types you can subscribe to that never arrive
Konsole lets you tick them. Nothing is ever delivered.
| Type | Why |
|---|---|
payment.checkout_page_opened | Recorded in the Konsole event log, never dispatched to endpoints |
payment.redirected_to_callback | Same, it is a checkout timeline entry |
link.expired | Emitted by the per link webhook system, not by account endpoints |
The first two are visible in Events, so you can still audit a checkout after the fact. You just cannot be notified of them.
Sandbox and live are separate streams
An endpoint belongs to one environment. A sandbox endpoint only ever receives sandbox events, and a live endpoint only live ones. Two things change in the payload:
livemodeisfalsein sandbox andtruein live.- Event ids are prefixed
evt_test_in sandbox andevt_in live.
A handler that hardcodes an id length or slices a prefix off will break when it meets the other environment.
Sync platforms receive a copy
When a payment, refund or transfer belongs to a merchant connected through Sync, the event is
delivered twice: once to the merchant's own endpoints, and once to the platform's, as a separate
event with its own id. The platform's copy carries an extra account object so you know which
connected merchant it came from.
"account": {
"id": "acc_7Yh2MpL4tRb3nP8sZcXv",
"reference": "boutique-akwa"
}Deduplicate on the event id, not on the payment id: the two copies describe the same payment and carry two different event ids, deliberately.