Test mode
Test keys come before live ones, and a test payment can be made to happen, so the whole integration including the webhook is testable before you go live.
#The key is the switch
The key decides the mode, not the dashboard toggle. A merchant flipping their dashboard to test does not change what their live key does to their production traffic. Verified.
Nothing to set on a request. Every response carries the mode it ran in.
#What is separate
| Keys | Two per mode, pk_ and sk_. The mode is in the string, so a test key left in production is obvious on sight rather than at the first payment that fails to arrive |
| Webhook endpoint | One URL and one signing secret per mode. A test integration can never post into a live system |
| Customer accounts | Issued per mode. The mode field on the account says which |
| Balances | GET /balance answers for the mode your key belongs to, and says so |
| Payments and checkouts | A session belongs to the mode that created it. GET /payments/{reference} is scoped to your merchant and your mode |
#Live keys arrive with verification
There is no sk_live_ until your business is approved. Test mode keeps working throughout, and switching back to test afterwards is always allowed.
#Making a payment happen
A test account number is generated here rather than at a bank, so nobody can transfer into it. Two endpoints stand in for the transfer, and they run the real credit path.
{ "outcome": "paid", "amount": 20000, "payer_name": "Ada Obi" }The collection.success webhook that follows carries that customer’s own customer_reference, which is what your server matches on in production. Full detail on simulate_payment and sandbox/pay.
#The outcomes
outcome | What happens | What it tests |
|---|---|---|
paid | credited, collection.success fires | the happy path |
underpaid | credited at the lower amount, webhook fires with what was really sent | the case integrations get wrong |
overpaid | credited at the higher amount | refund and reconciliation logic |
expired | nothing credited, no webhook at all | timeout handling |
There is deliberately no failed. A bank transfer does not fail: the payer either sends the money or they do not, and there is no collection.failed event in this system because nothing could emit one. A button producing a webhook live will never send would teach a developer to write a branch that never runs and then trust it. transfer.failed is real, but that is a payout.
underpaid is the one worth building a test around. An integration that assumes amount == expected ships a bug that surfaces the first time a real customer fat-fingers a transfer.
#It runs the real path
Simulation runs the same code path a live payment runs. The ledger entry, the fee, the payer record, the checkout match and the webhook are the live sequence with mode set to test. A simulator with its own copy of that logic would drift, and the day it drifted a merchant would certify their integration against behaviour we no longer have.
So the collection fee is charged in test too. A sandbox that paid out gross would surprise a merchant at their first real settlement.
#From the checkout page instead
A test-mode checkout renders the same four outcomes as buttons, so a developer who is not driving the API by hand can click through the whole flow. Same mechanism, second door.
#What test mode still cannot do
Account creation in test mode is instant and local, so it never fails. In live it is a real our bank partner call that can time out or return a provider error, so a developer cannot yet exercise their own error handling for account creation and will meet their first failure in production. Worth closing later, behind this, because a webhook that never fires costs a merchant money and a creation error costs them a retry.
#When you switch over
- Swap the key on your server. That is the only code change.
- Point your live webhook URL at your handler and copy the live signing secret. They are different values from the test ones.
- Expect an empty balance and no history: figures do not carry across from test.