Sandbox and testing
https://sandbox.paysigna.comThe sandbox is the gateway, not a mock. Same binaries, same database schema, same risk rules, same ledger, same settlement batches, same signed webhook delivery. Exactly one thing is substituted: the rail at the far end that would have moved the money.
Your sandbox activity is kept strictly apart from your live activity throughout — a test payment is delivered only to your test webhook endpoints, accrues only into test settlement batches, and is counted only in test figures.
That matters because the alternative — a hand-written mock — diverges from the real thing exactly where it is least convenient to find out. A flow that works here works live.
| Sandbox | Live | |
|---|---|---|
| Host | sandbox.paysigna.com |
api.paysigna.com |
| Keys | psg_test_… |
psg_live_… |
| Rail | A simulator we run | Real partners |
| Amount limits | None | Your limits, and the partners’ |
| Instruments | Any number, account or card | Real ones |
| Money | None | Real |
| KYC required | No | Yes — see Going live |
| Webhooks | Delivered normally, signed normally, to your test endpoints | Same, to your live ones |
| Emails and SMS | Prefixed [TEST] |
Not prefixed |
Keys are not interchangeable. A psg_test_… key against api.paysigna.com
returns 403 with code environment_mismatch, and so does the reverse. The
environment is baked into the key string so you can catch that by looking.
There are no magic numbers and no amount ceiling
Section titled “There are no magic numbers and no amount ceiling”Send any amount. One pesewa or ten million cedis; the sandbox has no band of its own. If your checkout sells a GHS 4,000 item, test it with GHS 4,000.
Use any instrument. Any mobile number on any network, any bank account in any format, any card number. There is no list of approved test cards to look up and nothing to memorise.
This is deliberate and it is a change from how most gateways in this market work. A vendor’s own test environment caps the amount and authorises four specific PANs, which means the one thing you cannot test there is your own product. We run the sandbox rail ourselves so that none of those restrictions reach you.
Two things the sandbox still enforces, because they are not restrictions:
- The amount must be positive. A zero or negative charge is not a small payment; it is a request the rest of the arithmetic has no meaning for.
- A payout cannot exceed your test balance. The ledger is real, and a sandbox that allowed an overdraft would be teaching your integration that overdrafts exist. Collect first — for any amount you like — and pay out of that.
Choosing what happens
Section titled “Choosing what happens”By default a sandbox transaction succeeds. To get anything else, set the
sandbox_outcome key in the request’s metadata:
{ "amount": 400000, "currency": "GHS", "channel": "mobile_money", "customer": { "identifier": "0244000000", "name": "Ama Mensah" }, "metadata": { "sandbox_outcome": "insufficient_funds" }}sandbox_outcome |
What happens | failure_class |
|---|---|---|
absent, or succeed |
Accepted, then succeeds a moment later | — |
pending |
Stays in flight for ten minutes, then times out | timeout |
indeterminate |
The create call fails with partner_timeout and the charge is left unresolved |
set at expiry |
insufficient_funds |
Fails immediately | insufficient_funds |
invalid_recipient |
Fails immediately | invalid_recipient |
limit_exceeded |
Fails immediately | limit_exceeded |
declined |
Fails immediately | declined |
timeout |
Fails immediately | timeout |
transport |
Fails immediately | transport |
partner_unavailable |
Fails immediately | partner_unavailable |
duplicate |
Fails immediately | duplicate |
Metadata rather than a magic amount or a magic card number, so that choosing an outcome never costs you the ability to send the amount you actually wanted to test. It works the same on a collection and on a payout.
failure_class is the value you will see on the transaction, in the webhook and
in the error body — the same vocabulary live uses, not a sandbox dialect.
An unrecognised value succeeds. A typo should not be the reason you spend an afternoon on a failure you did not ask for.
How long it takes
Section titled “How long it takes”A sandbox collection is accepted immediately and settles about three seconds later, because a real mobile-money charge is asynchronous and the sandbox does not pretend otherwise. The sequence is the one live uses:
POST /v1/chargesreturnsawaiting_action(mobile money) orpending.- A moment later the transaction completes, the ledger posts, and your balance moves.
- Your webhook endpoint is called, signed.
So the create response is not where you learn the outcome — in the sandbox or in live. Build on the webhook from the start; it is the only ordering that is correct in production.
Mobile money, bank and card
Section titled “Mobile money, bank and card”All three channels work, for collections and payouts.
Mobile money returns an awaiting_action status with an action type of prompt
and no action URL, which is exactly what live returns: on a real rail the
approval message is already on the customer’s handset and there is nothing for you
to render. Card returns pending with no redirect, because a fake 3-D Secure page
would teach your integration to send customers to a URL we do not issue in live.
Testing from this site
Section titled “Testing from this site”The API reference has a request console built in. Open an
operation, choose Test Request, put a test secret key in the
Authentication panel, and send it. The response you see is the real response
from sandbox.paysigna.com.
Three things worth knowing about it:
It only ever targets the sandbox. The live host is not in the server list, and not because it is hidden in the interface — it is absent from the OpenAPI document this page loads, so there is nothing to select. The reasoning is in Going live; the short version is that a request console is a browser, and a secret key in a browser is the thing publishable keys exist to prevent.
Your key is not stored. The console does not persist what you type into the authentication panel, so you will paste the key again next visit. That is deliberate: persisting it would mean a payment credential sitting in this site’s web storage, readable by any script that ever got injected into this page. One paste per session is the cheaper side of that trade.
Use a test key anyway. Even against the sandbox, treat the key in your clipboard as a credential. Do not paste a live key into this page; it will not work, and you will have put it somewhere it did not need to go.
When the console cannot reach the sandbox
Section titled “When the console cannot reach the sandbox”A browser will not let this page read a response from another origin unless that
origin says it may. If sandbox.paysigna.com does not list
https://docs.paysigna.com in its allowed origins, every request from the console
fails — and it fails in a way that looks nothing like the real cause.
What you see is a request that never returns a status, or a generic “Failed to
fetch” / “Network error”, with a CORS message in the browser console. What it is
not is a bad key, a wrong body or a down API. The giveaway is that the same
curl works perfectly from your terminal, because curl is not a browser and never
asked permission.
If you hit that, the sandbox deployment needs this site’s origin added to
HTTP_CORS_ORIGINS — that is our configuration to fix, not yours. Tell us at
[email protected] and use curl or your own code
in the meantime.
Testing the parts that are hard to trigger
Section titled “Testing the parts that are hard to trigger”Webhook deliveries. You do not need to make a payment succeed to test your
handler. POST /v1/webhook-deliveries/{id}/replay re-sends a delivery you have
already had, signed freshly, so you can iterate on your verification code against
a real signature.
Backfilling after an outage. GET /v1/events returns everything we tried to
send you, with data byte-for-byte as it was signed — so a stored signature still
verifies against it. Reading it sends nothing, which means you can recover without
your handler being hit by a burst of replays. Pass after_sequence to walk
oldest-first from the last sequence you processed; that is the only order in
which applying missed events is correct.
Failure paths. Rail failures come from sandbox_outcome above. The rest of
the error codes in Errors are provoked by malforming the request on
purpose: reuse an Idempotency-Key with a different body for
idempotency_key_reused, present a key without the scope for
insufficient_scope. Build the branches before you need them live.
Emails and SMS we send. Every notification from the sandbox is prefixed
[TEST] in its subject line — or at the front of the message, for an SMS — and
carries a TEST MODE badge in the body. A sandbox dispute notice forwarded to
somebody’s finance team should be obvious at a glance from the inbox list, not only
after it has been opened. If you ever receive an unprefixed message from a test
key, tell us: that is a bug on our side, not a quirk.
Local webhook development. Your endpoint URL must be HTTPS and publicly routable — loopback, private and link-local addresses are refused, because our delivery workers run inside our own network and could not reach them anyway. Use a tunnel for local work, and register the tunnel URL as a test-environment endpoint.
Before you move on
Section titled “Before you move on”Your integration is sandbox-complete when all four of these are true:
- A successful collection updates your own records from the webhook, not from the create response.
- A retried create with the same
Idempotency-Keydoes not produce a second payment. - Your webhook handler verifies the signature, deduplicates on event
id, and returns inside its timeout. - You reconcile against
merchant_amount, notamount.
Then read Going live.

