Skip to main content
Sandbox behaves like production, it just doesn’t move real money. What it gives you that production can’t is reserved numbers: send to one and you get that outcome on demand. That matters because the cases your integration most needs to handle — a payout that fails, one that never settles, a frozen account — are the ones you can’t reproduce against a real provider. A provider sandbox only tells you what it happens to know about, and is generally happy to succeed.
Reserved numbers mean nothing in production — they’re ordinary numbers there. Make sure none of them survive into your production configuration.

Getting set up

Sandbox accounts are self-service. Register on the backoffice, generate a key, and point at https://sandbox-api.kotanipay.com. See the Quickstart.

These work in every country we support

Reserved phone numbers are matched on their last nine digits only. Put your own corridor’s country code in front and you get the same outcome: All four resolve to the same reserved outcome. So the tables below list the nine-digit ending — prefix it with the country code for the corridor you’re testing, in international format.
This applies to phone numbers only. The bank account numbers and shortcodes further down are matched exactly as written.

Payment outcomes

Use as the customer phone number on a deposit, payout or onramp. These behave like production, not like a mock. The transaction is accepted, sits pending, then settles — firing the same webhook, moving the same balances and writing the same timeline as a real one. A failed payout reverses the wallet debit, because a real failed payout does. Any number that isn’t reserved succeeds after the usual delay, so you only need a reserved number when you want a particular outcome.

Worth testing

…700000010. Confirm you handle the FAILED webhook, surface the reason, and notice the wallet debit is reversed — the balance returns to what it was.
…700000020 stays pending forever. This is the one most integrations get wrong: money is committed but no terminal webhook ever arrives. Make sure something on your side eventually reconciles or escalates rather than waiting indefinitely.
…700000021 takes about a minute. Checks that you aren’t treating a transaction as final just because the create call returned, and that your polling backs off sensibly.
…700000002 succeeds but is registered to a different name than …700000001. Pair it with the validation numbers below to catch a mismatched recipient before you pay.
Deposits credit your wallet on the same reconciliation pass production uses, so the balance moves within about a minute of the deposit settling rather than instantly.

Bank and card payments

These aren’t simulated. Bank and card providers run their own sandbox environments, so those transactions dispatch for real outside production and are tested against the provider’s own test credentials and test cards. Reserved numbers here would take that away.

Validation outcomes

Different numbers, for Account Validation rather than payments. These return a fixed result without contacting a provider.

Phone numbers

Same nine-digit rule as above — prefix with your corridor’s country code.

Bank accounts

Paybill and till shortcodes

Checking a name before you pay

Send the name you expect and the response scores it against the real one. Names are compared token by token, so middle names, initials and ordering still match — Jane M Doe matches JANE MARY DOE. Pair account 1000000002 with an expected name of Jane Doe to see a failed match: the account is real and isValid stays true, but nameMatch.matched is false.
When the response marks the name as masked, the provider returned only part of it, so the match is based on the visible portion alone. Treat it as “not contradicted” rather than confirmed.
Resolved names and match scores require name lookup to be enabled on your account. Without it you still get format and corridor validation, and nameLookupStatus comes back as NOT_ENABLED. Talk to your account manager to have it turned on.