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.
status | What it means | Webhook | What to do |
|---|---|---|---|
pending | The payment exists. No operator has been contacted yet. | payment.created | Redirect the customer, or make the charge call. Nothing to fulfil. |
processing | Sent to the operator. The customer is being prompted on their phone. | payment.processing | Wait. Do not retry and do not create a second payment. |
partial | Some instalments of a split payment are settled, others are not. | payment.split.* | Wait. amount_paid on the payment says what has settled so far. |
succeeded | Funds captured. | payment.succeeded | Fulfil the order. |
failed | The attempt did not go through. | payment.failed | Read failure_reason, then decide whether a retry makes sense. |
cancelled | Cancelled 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.cancelled | Offer to start a new payment. |
expired | The payment window elapsed before the customer acted. | payment.expired | Create 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.
| From | Can become |
|---|---|
pending | processing · partial · succeeded · failed · cancelled · expired |
processing | pending · partial · succeeded · failed · cancelled · expired |
partial | processing · succeeded · failed |
failed · cancelled · expired | succeeded 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.
Never retry a processing payment
Creating a second payment because the first one is "taking too long" is the most common way to
charge a customer twice. A payment in processing is still live. Wait for payment.succeeded,
payment.failed, or payment.expired.
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:
| Minimum | 5 minutes |
| Default | 1 440 minutes (24 hours) |
| Maximum | 43 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.
https://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_code | When | What to do |
|---|---|---|
payment_in_progress | A charge is under way: the payer is approving it on their phone, or paying on the provider's page | Wait for its outcome, by webhook or GET /payments/{id}. A payment that comes back to pending can be cancelled |
payment_partly_paid | Instalments of it were paid | Close 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
https://api.wajub.com/payments| Query parameter | Accepts |
|---|---|
status | pending, processing, partial, succeeded, failed, cancelled, expired |
reference | Exactly the reference you sent at creation. It is not unique, so every payment that carries it comes back |
search | Free text over the payment id, your reference and the description |
date_from, date_to | Dates, compared with the creation date |
amount_min, amount_max | Amounts in the major unit of the currency |
per_page | 1 to 100, defaults to 25 |
cursor | Switches 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=50The 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.