Transaction statuses
Charges, payouts, deposits and withdrawals all move through the same small set of statuses.
INITIATED ─▶ PENDING_PROVIDER ─▶ AWAITING_CUSTOMER ─▶ SUCCESSFUL ─▶ REVERSED
│ │
└────────────────────┴──────▶ FAILED| Status | Meaning | Final? |
|---|---|---|
INITIATED | Received. You will rarely see it, except on a withdrawal waiting for a second approver. | No |
PENDING_PROVIDER | Handed to the mobile money network; waiting for its first answer. | No |
AWAITING_CUSTOMER | The prompt is on the customer's phone, or the network is still working on it. | No |
SUCCESSFUL | The money moved. | Yes, unless later reversed |
FAILED | The money did not move. failure_code says why. | Yes |
REVERSED | A successful charge was clawed back. | Yes |
Out-of-order and duplicate updates
Networks can report the same outcome twice, or report it late. SkanPay records every report in the transaction's timeline, but a final status never changes afterwards except SUCCESSFUL → REVERSED. A late “pending” after “successful” is recorded and ignored; it never moves money. Apply the same rule in your own system: never let an update move an order backwards.
Failure codes
A charge that reaches the network but does not go through is not an HTTP error. It is a FAILED transaction with a failure_code and a human-readable failure_message. Show the customer a message based on the code, not the raw message.
| failure_code | Meaning | Suggested handling |
|---|---|---|
insufficient_funds | The wallet does not have enough balance. | Ask the customer to top up and try again. |
customer_declined | The customer rejected the prompt. | Offer to try again. |
customer_unreachable | The handset could not be reached (off, out of coverage). | Ask the customer to check their phone and retry. |
wallet_not_found | There is no mobile money wallet on that number. | Check the number and the network. |
wallet_limit_exceeded | The payment would exceed the customer's own telco limit. | Try a smaller amount. |
wallet_inactive | The wallet exists but is not active. | The customer must contact their network. |
invalid_msisdn | The network rejected the number. | Check the number. |
expired | No answer arrived in time (about 30 minutes). | Treat as not paid. A new attempt needs a new charge. |
provider_rejected | The rail refused the request outright. | Retry later with a new Idempotency-Key; contact support if it persists. |
provider_unavailable | The rail was down. | Retry later. |
unknown | The rail failed the payment without giving a reason. | Treat as not paid. |
Withdrawals can also fail with approval_rejected when a team member rejects them in the dashboard. Treat any code you do not recognise like unknown: new codes may be added.
Who reported each change
The timeline (GET /v1/transactions/:id) shows every status event with a source:
| source | Meaning |
|---|---|
API | Set while handling your request. |
PROVIDER_CALLBACK | The network told SkanPay. |
POLL | SkanPay asked the network. |
RECONCILIATION | Decided by the reconciliation job, for example an expiry. |
ADMIN | Resolved by SkanPay staff, always with a recorded reason. |