Sandbox & Testing
The sandbox is the same platform as production, running the same code, with payment providers replaced by a simulator. Use it to build and test your integration; no money moves.
Sandbox vs Production
| Sandbox | Production | |
|---|---|---|
| Base URL | https://sandbox.4pay.online/api/v1 | https://4pay.online/api/v1 |
| Payment page | https://pay.sandbox.4pay.online | https://pay.4pay.online |
| Credentials | Separate key and organization id | Separate key and organization id |
| Real charges | ❌ Never | ✅ Real money |
| Webhooks | ✅ Sent to your terminal's notify_url | ✅ Sent to your terminal's notify_url |
The environments are separate deployments on separate hostnames, and credentials do not carry across: a sandbox key is meaningless in production and the other way round. Keys have no prefix that tells them apart — keep them in separate configuration.
Inside a host, env in the request body picks the environment of the transaction itself, and it
is required. A ?env=test query parameter only scopes searches: a create carrying just that
produces a production transaction. See Transactions.
Getting sandbox credentials
Ask your platform operator. They create a partner account for you in a sandbox organization and hand you two values together — the API key and the organization id. Both are needed on every request.
Making a test transaction
curl -X POST https://sandbox.4pay.online/api/v1/transactions \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_SANDBOX_KEY" \
-H "x-organization-id: YOUR_SANDBOX_ORGANIZATION_ID" \
-d '{
"params": {
"type": "payment",
"amount": "1000",
"currency": "USD",
"txid": "sandbox-test-001",
"env": "test",
"hosted": true,
"returnUrl": "https://yoursite.com/success",
"failUrl": "https://yoursite.com/fail"
}
}'
You get back a widget_url. Open it in a browser to see the hosted payment form, and pay with
one of the cards below.
Test cards
The simulator does not classify a card by its BIN the way public gateways do. It compares the
whole tuple — number, CVV, cardholder and expiry — against a fixed list. One character of
difference is refused with unexpected card.
| Card number | CVV | Cardholder | Expiry | Result |
|---|---|---|---|---|
2201382000000013 | 283 | IVAN IVANOV | 12 / 20 | ✅ Settles — charging, then charged within about a minute |
2201382000000021 | 749 | IVAN IVANOV | 12 / 20 | ❌ rejected by the provider |
2201382000000047 | 480 | IVAN IVANOV | 12 / 20 | ↩️ Settles, then refunds |
2201382000000039 | 572 | IVAN IVANOV | 12 / 20 | ❌ failed |
2201382000000054 | 111 | IVAN IVANOV | 12 / 20 | ⏸ Payout stuck in payout_suspended |
Anything else — including the Visa and Mastercard test numbers other gateways use — ends as
rejected with unexpected card.
"money_storage": {
"cardnumber": "2201382000000013",
"cvv": "283",
"cardholder": "IVAN IVANOV",
"exp_year": "20",
"exp_month": "12"
}
Note exp_year is two digits, and the cardholder is upper-case. The
SDK exports these as constants (sandbox.CARD_CHARGE and friends) so you
do not retype them.
Payouts
| Field | Value | Result |
|---|---|---|
bank_id | 200000000001 | Payout settles |
bank_id | 200000000002 | Payout refused |
customer (SBP) | John Doe | Settles; any other name is refused |
Charging without a browser
For automated tests, skip the payment page and charge the card server-to-server:
curl -X POST https://sandbox.4pay.online/api/v1/transactions \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_SANDBOX_KEY" \
-H "x-organization-id: YOUR_SANDBOX_ORGANIZATION_ID" \
-d '{
"params": {
"type": "payment",
"amount": "1000",
"currency": "USD",
"txid": "sandbox-test-002",
"env": "test",
"show_widget": false,
"money_storage": {
"cardnumber": "2201382000000013",
"cvv": "283",
"cardholder": "IVAN IVANOV",
"exp_year": "20",
"exp_month": "12"
},
"returnUrl": "https://yoursite.com/success",
"failUrl": "https://yoursite.com/fail"
}
}'
It returns charging and reaches charged on its own within about a minute. Both redirect URLs
are required here — a server-to-server payment without them is created and immediately rejected.
Passing a real card number through your own server means the card data environment is yours to secure and audit. In the sandbox this is a convenience for tests; in production, use the hosted flow unless you already hold a PCI DSS attestation.
Testing webhooks
Expose your local server with a tunnel:
ngrok http 3000
# → Forwarding: https://abc123.ngrok.io → localhost:3000
Ask your operator to set https://abc123.ngrok.io/webhooks/payment as the notify_url on your
sandbox terminal, with a webhook secret — without one the platform sends no signature and you
cannot tell a genuine delivery from a forged one. Details in Webhooks.
What is not simulated
| Provider-specific behaviour | The simulator stands in for every provider. Rail-specific quirks — 3DS variants, local wallet redirects, bank cut-off times — appear only in production |
| Settlement timing | Sandbox payments settle in seconds; real rails take minutes to days |
| Fees | Fee calculation runs, but with whatever rates the sandbox organization has configured |
Going live
- Get production credentials from your operator — a new key and organization id.
- Change the host to
https://4pay.online. - Change
envin the request body to"prod". - Point
notify_urlat your production endpoint, with its own secret. - Run one real payment for a small amount, and refund it.
Steps 2 and 3 are separate on purpose. The host decides which platform you are talking to; env
decides which side of that platform the transaction lives on. Getting them out of step is the
mistake that produces a "test" payment on a live rail.