Test mode
Build and test the entire integration, including payments, payouts, refunds, webhooks and failures, before anyone verifies anything and without a franc moving.
How it works
Create a test app, take a test key, and use the same base URL and the same endpoints. Test-mode transactions never reach MTN or Airtel: they are simulated inside our service, end to end. Nobody is prompted, nothing is charged, and the webhooks, ledger entries and dashboard rows are produced by exactly the same code that produces the real ones.
Same code, simulated rails
Register your test numbers first
Test mode will only transact with numbers on your test list. Anything else is refused with a 400.
This obstacle is the point
Choosing the outcome
A sandbox where everything succeeds only exercises the path that was never going to cost you anything. The last digit of the amount picks the ending:
| Amount ends in | What happens | Status |
|---|---|---|
1 | The customer declines immediately. | failed, synchronously |
2 | The customer never answers. The prompt expires about a minute later, like a real one. | pending → failed |
| any other digit | The customer approves. | succeeded |
5000 → succeeded
5001 → failed straight away
5002 → pending now, failed in about a minute
20000 → succeeded
100 → succeeded (and 100 RWF is the minimum, in both modes)The …2 case is the valuable one. It is the state your code spends most of its real life in, and the one most integrations have never actually seen: a payment that sits pending, and a payment.failed webhook arriving minutes after the customer has closed the tab.
What is identical to live
| Validation | Same minimum, same number format, same refusals. A request test mode accepts is one production accepts. |
| Webhooks | Same events, same signing, same retry schedule. Point them at a tunnel on your laptop. |
| Idempotency | Same replay behaviour, same unique index. |
| Fees and ledger | Calculated and posted the same way, against a test ledger. |
| Dashboard | Same screens and filters, with a loud Test badge on everything. |
What is different
| Nothing leaves the building | No gateway call, no prompt, no money. |
| Outcomes are yours to choose | The last-digit rule above, rather than whatever a customer does. |
| The balance is not money | A test balance cannot be withdrawn. It is a number in a simulated ledger. |
| Instant | Approval happens at the speed of a function call, not a person. |
Going live
Get verified, create a live app, swap the key. That is the entire migration. The base URL, the request bodies, the webhook format and your handler code are unchanged, because there was never a second API to migrate from.
Before you flip it
xpay_test_ versus xpay_live_) precisely so that this is a one-second check rather than an archaeology exercise. A payment sitting in your test dashboard when a customer says they paid is the most common version of this mistake.