Transaction lifecycle
Collections, payouts, and refunds are all transactions and share one lifecycle.
Statuses
accepted ──> submitted ──> completed
│ │
│ ├────────> failed
│ │
│ └────────> expired
└─────────────────────> failed (rejected at submission)| Status | Meaning |
|---|---|
accepted | We validated and recorded your request. Nothing has reached the mobile-money operator yet. |
submitted | Handed to the operator. For a collection, the customer is being prompted on their phone. |
completed | Terminal. The money moved. |
failed | Terminal. It did not, and will not. A reason code says why. |
expired | Terminal. No outcome within the time limit — typically the customer never responded to the prompt. |
Terminal means terminal
completed, failed, and expired are absorbing: once reached, a transaction's status never changes again. Not by a late operator callback, not by a retry, not by anything.
This matters for your integration: once you observe a terminal status you can act on it irreversibly — ship the goods, credit the account — without worrying that it will be reversed later. If an operator subsequently reports something contradictory, we record that contradiction for reconciliation and raise it internally; your transaction's status stays as it was.
Why you should poll
Operator callbacks are a hint, never proof. Mobile-money networks deliver them late, twice, or not at all, and a callback claiming success is not evidence that money moved.
So MoniWave does not treat a callback as authoritative either. A callback triggers a status check against the operator, and only that confirmed check drives a transaction to a terminal state.
For you, the rule is the same:
GETthe transaction is authoritative. Whatever it returns is the truth.- Webhooks are a convenience so you don't have to poll in a tight loop. Every event obtainable by webhook is obtainable by polling.
- Never build an integration that only works if webhooks arrive. A missed webhook must never mean a lost transaction — poll for anything still non-terminal after a reasonable interval.
Timing
| Phase | Typical | Notes |
|---|---|---|
accepted → submitted | under a second | We hand off asynchronously. |
submitted → terminal | seconds to a few minutes | Depends on how fast the customer responds. |
→ expired | ~15 minutes | Non-terminal transactions are expired by a sweeper. |
Poll with backoff rather than tightly: a few seconds initially, widening to ~30–60 s. There is no benefit to polling faster than the customer can physically approve a prompt.
Every transaction keeps a full history
Internally, a transaction's status is not a stored field that gets overwritten — it is derived from an append-only sequence of recorded facts: requested, submitted, callback received, status checked, outcome confirmed, money posted, webhook dispatched.
Two consequences you can rely on:
- Nothing is silently overwritten. Every state change has a recorded cause, retained for disputes.
- We can always answer "what did you know and when?" — useful when reconciling with an operator or investigating a customer complaint.
The dashboard shows this timeline on each transaction's detail page, with the actor behind every step. While a transaction is still live the page follows it, so you can watch it settle rather than reloading.
Refunds do not change the original
A refund is its own transaction with its own lifecycle, linked to the original collection. Refunding a collection never changes that collection's status: a fully refunded collection stays completed, and the refund appears alongside it. See refunds.md.