Skip to content

Payment lifecycle

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

Every state a payment can hold, the webhook that announces it, and what your integration does in each. Two of them mean "wait": pending, before any operator is contacted, and processing, while the customer is being prompted, where a retry 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. succeeded never moves again, except through refunds: a refunded payment reads refunded or partially_refunded.

A prepared payment, before activation

A payment created with deferred: true, for payment fields shown before your order exists, is only prepared until your server activates it with POST /payments/{id}/activate. Until then it sends no payment.* webhook and stays out of GET /payments and the Dashboard. Read by its id, it is pending, then expired or cancelled, and it cannot be charged. Activation sends payment.created, and the payment then follows the states above, with the same id and expiry. Prepared payments sums up the differences.

Which transitions are possible

Wajub only moves a payment along the paths below.

FromCan become
pendingprocessing · partial · succeeded · failed · cancelled · expired
processingpending · partial · succeeded · failed · cancelled · expired
partialprocessing · succeeded · failed
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. When a late confirmation cannot be credited, for instance because another attempt had already collected the payment, the payer is refunded and the payment stays as it was. Nothing else leaves those three states, so another attempt after them means a new payment.

Three 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 through its refunds

A payment reads partially_refunded once refunds that succeeded cover part of it, and refunded once they cover all of it. 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 and refund.failed, which also arrives, with cancelled_by_wajub, for a refund Wajub stopped before sending it. To know how much of a payment came back, list its refunds with GET /payments/{id}/refunds and sum the ones that succeeded.

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.

payment.expired can arrive a little after expires_at, and the payment can still read pending in between. 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. A payment whose charge is still under way when expires_at passes, the payer approving it on their phone for instance, does not expire then: it waits for that charge. Paid, it is succeeded; refused at once, it is back to pending and can expire; refused later, it is failed, except a payer's own code the operator refuses later, which puts it back to pending, where it can expire too. A payment with an instalment already paid never expires: it stays open until it is settled or closed.

What you receive when a payment fails

A failed payment carries a failure_reason, and failure_message says it in one sentence. 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 nobody is paying 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. A payment that cannot be cancelled now answers 409, with its reason in error_code:

error_codeWhenWhat to do
payment_in_progressA charge is under way: the payer is approving it on their phone, or paying on the provider's pageWait for its outcome, by webhook or GET /payments/{id}. A payment that comes back to pending can be cancelled
payment_partly_paidInstalments of it were paidClose it with POST /payments/{id}/close, keeping or refunding what was collected

Any other payment, already paid, failed, cancelled or expired, 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: they list the payments refunded in full or in part, which succeeded then leaves out. 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: 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?