Skip to content

Payment lifecycle

Every state a payment holds, which webhook announces it, and what to do in each.

A payment does not succeed or fail the moment you create it. Between those two outcomes it passes through states that reflect what is happening on the customer's phone and at the mobile money operator. Your integration has to behave differently in each one.

Two of those states mean "wait", and telling them apart matters: one means Wajub has not contacted an operator yet, the other means the customer is being prompted right now. Retrying during the second one creates a second charge.

The states

Branch on `status`, never on the message

The message field is written for humans and can change without notice. The status value is part of the API contract and is what your code should switch on. Branch on it rather than on which event arrived, because more than one event can announce the same state.

statusWhat it meansWebhookWhat to do
pendingThe payment exists. No operator has been contacted yet.payment.createdRedirect the customer, or make the charge call. Nothing to fulfil.
processingSent to the operator. The customer is being prompted on their phone.payment.processingWait. Do not retry and do not create a second payment.
partialSome instalments of a split payment are settled, others are not.payment.split.*Wait. amount_paid on the payment says what has settled so far.
succeededFunds captured.payment.succeededFulfil the order.
failedThe attempt did not go through.payment.failedRead failure_reason, then decide whether a retry makes sense.
cancelledCancelled by you with DELETE /payments/{id}, or by the customer with the Cancel button of the hosted page. A decline on the handset is not a cancellation.payment.cancelledOffer to start a new payment.
expiredThe payment window elapsed before the customer acted.payment.expiredCreate a new payment. The old one can never complete.

Four of those end the attempt: succeeded, failed, cancelled and expired. Wajub stamps the moment to decide how long the hosted page stays reachable. succeeded never moves again, except through refunds: from API version 2026-10-11 a refunded payment reads refunded or partially_refunded.

Which transitions are possible

Wajub only moves a payment along the paths below. A status you did not expect after another one is a bug on our side, not a state you need to handle.

FromCan become
pendingprocessing · partial · succeeded · failed · cancelled · expired
processingpending · partial · succeeded · failed · cancelled · expired
partialprocessing · succeeded · failed · cancelled
failed · cancelled · expiredsucceeded only

Two of those paths surprise people. processing goes back to pending when the operator declines a charge at once, with a 402, so that the same payment can be charged again; payment.attempt_failed tells your server. And a failed, cancelled or expired payment can still become succeeded when the operator confirms the money late: fulfil on payment.succeeded whenever it arrives. It does so at once when your own provider collected it. When a Wajub rail did, or when another attempt had already collected the payment, Wajub reviews the late confirmation first: it either credits the payment, and payment.succeeded follows, or refunds the payer and the payment stays as it was. Nothing else leaves those three states, so another attempt after them means a new payment.

Four things that surprise people

processing means the customer's phone is ringing

This is the state your integration will spend the most time in, and the one most likely to be mishandled. On mobile money, processing means the operator has pushed a prompt to the customer's handset and is waiting for them to enter their PIN. That can take seconds, or the length of time it takes someone to find their phone.

A refund changes the payment's status only from API version 2026-10-11

This is the one that catches most integrations, and it depends on your API version. From 2026-10-11, a payment reads partially_refunded once refunds that succeeded cover part of it, and refunded once they cover all of it. On an earlier version it keeps reading succeeded, in full or in part, forever. Either way it is a reading of the refunds: the payment itself does not change, and there is no payment.refunded event. What the payment's status says

Refunds have their own event family instead: refund.created, refund.processing (live only), refund.succeeded, refund.failed and refund.cancelled. To know how much of a payment came back, list its refunds with GET /payments/{id}/refunds and sum the ones that succeeded.

expired is announced as payment.expired

An expired payment is announced by payment.expired, whether its window closed or a sandbox timeout number ended it. A sandbox timeout used to announce itself as payment.failed; it no longer does. Branch on status all the same: it is what the rule at the top of the page is for.

partial belongs to split payments only

A payment can be settled in two to four instalments, in sandbox and, on Mobile Money, in live (Split payments). While some are paid and others are not, its status is partial, processing while an instalment waits for the payer, and each instalment reports on its own through payment.split.processing, payment.split.succeeded and payment.split.failed.

There is no payment.partial event, and a subscription matches exact names only: payment.* matches nothing. List the three payment.split events by name, or subscribe to *. The payment carries no list of its instalments: amount_paid is the authoritative record of what has settled, and each payment.split.* event tells you which instalment moved.

A partial payment stays open until its last instalment is paid: nothing expires or cancels it. To stop before, close it with POST /payments/{id}/close: it becomes succeeded for what was collected, which you keep or refund (outcome). DELETE /payments/{id} refuses a payment with instalments already paid.

Expiry

Every payment carries a deadline, set with expires.in at creation, in minutes, and read back in its expires_at:

Minimum5 minutes
Default1 440 minutes (24 hours)
Maximum43 200 minutes (30 days)

The floor is five minutes because a mobile money prompt needs time to reach the handset and be confirmed. Shorter windows expire before the customer can pay.

Expiry is not instantaneous: it can lag the deadline by a few minutes. A payment whose deadline has just passed can still read pending for a moment, and payment.expired arrives a little after the deadline rather than on the exact second.

That delay is a feature, not a rounding error. Do not compute expiry yourself to decide an order is dead: an operator can still confirm a payment in the final seconds of its window, and expiry only applies to payments that were never credited. Nor does it apply to a payment paid in instalments once one of them is paid: that money is the payer's, and the payment stays open for the next instalment. A partial payment never expires: it stays open until it is settled or cancelled.

What you receive when a payment fails

A failed payment carries a failure_reason, and failure_message says it in one sentence. The value is the code of the provider that refused the last attempt, and Wajub routes across many providers, or payment_failed when that attempt failed without a provider code. See Failure Reasons.

Branch on status for control flow. Use failure_reason to explain to a human what happened, and to decide manually whether a class of failure is worth retrying.

Cancelling a payment

A payment that is pending, processing or partial can be cancelled from your server, when the customer abandons the order on your side.

DELETEhttps://api.wajub.com/payments/{id}

The payment moves to cancelled, payment.cancelled is sent, and its page stops accepting payment. Any other status answers 400 This payment cannot be cancelled (already completed or expired). A confirmation the operator sends after the cancel still moves the payment to succeeded, as described above.

Listing payments by state

GEThttps://api.wajub.com/payments
Query parameterAccepts
statuspending, processing, partial, succeeded, failed, cancelled, expired
referenceExactly the reference you sent at creation. It is not unique, so every payment that carries it comes back
searchFree text over the payment id, your reference and the description
date_from, date_toDates, compared with the creation date
amount_min, amount_maxAmounts in the major unit of the currency
per_page1 to 100, defaults to 25
cursorSwitches to cursor pagination

status also accepts refunded and partially_refunded. From API version 2026-10-11 they list the payments refunded in full or in part, which succeeded then leaves out. On an earlier version they return an empty list. What the payment's status says

To see the payments still waiting on a customer, filter on pending:

curl -G https://api.wajub.com/payments \
  -H "Authorization: $WAJUB_API_KEY" \
  -d status=pending \
  -d per_page=50

The receipt of a succeeded payment

Once a payment has succeeded, it carries receipt_url: the payer's receipt, a page hosted on your invoice domain, in the payer's language. The link is signed, and the signature in its s parameter is what opens it, so pass the URL on as it is.

The same document comes in two other forms. Add /pdf to the path for a PDF, or /data for the receipt as JSON, and keep the query string.

What did you think of this content?