Get started
Sandbox & test mode
Every BaxPay account has two parallel worlds: test and live. Same API, same features, completely separate data — and in test mode, no real money ever moves.
How test mode works
Every object in BaxPay — API key, connection, session, webhook endpoint, event — carries a
livemode flag. livemode: false objects live in the sandbox; livemode: true objects are the
real thing. The two sets never mix: a test key cannot see live data, a test session can never
reach a live webhook endpoint, and vice versa.
API keysprefixSecret keys are minted per mode: bx_test_… for the sandbox, bx_live_… for production. The
key's mode decides the mode of everything you create with it — there is no way to accidentally
charge a real card with a test key.
Dashboardper tabThe dashboard is live until you switch a tab to Sandbox; that tab then shows test data on
every page, while your other tabs, your team and your tills stay live
(details). Under the hood each request carries
X-Baxpay-Mode: test | live (default live).
Webhooksmode-scopedWebhook endpoints are registered in one mode and only ever receive that mode's events. Every
event payload carries livemode, so your receiver can assert it defensively.
Simulating outcomes
Live sessions are completed by a shopper at a real terminal (fingerprint + PIN). Test sessions have no shopper, so the sandbox gives you a lever instead:
/integration/sessions/{id}/simulatetest sessions only{ "outcome": "APPROVED" }outcome is one of APPROVED, DECLINED or TIMEOUT — enough to exercise your happy path,
your failure handling and your stuck-basket recovery. Calling it on a live session is rejected:
real money only moves through the real terminal flow.
Test cards
Test links and invoices take these cards, and only these: any other number is refused, so a real card typed into a test page is never processed. Use any future expiry, any 3-digit CVC (4 digits for American Express) and any name. The numbers follow the usual industry test-card conventions, but every outcome is decided by BaxPay: a test payment never reaches an acquirer or a card network. On a test pay page, Test cards fills the form with one of them.
| Number | Brand | Result | code |
|---|---|---|---|
4242 4242 4242 4242 | Visa | Succeeds | |
5555 5555 5555 4444 | Mastercard | Succeeds | |
3782 822463 10005 | American Express | Succeeds | |
4000 0025 0000 1001 | Cartes Bancaires | Succeeds | |
4000 0025 0000 3155 | Visa | 3D Secure, then succeeds | |
4000 0000 0000 3220 | Visa | 3D Secure 2, then succeeds | |
4000 0084 0000 1629 | Visa | 3D Secure, then declined | card_declined |
4000 0000 0000 0002 | Visa | Declined | card_declined |
4000 0000 0000 9995 | Visa | Insufficient funds | insufficient_funds |
4000 0000 0000 9987 | Visa | Lost card | lost_card |
4000 0000 0000 9979 | Visa | Stolen card | stolen_card |
4000 0000 0000 0069 | Visa | Expired card | expired_card |
4000 0000 0000 0127 | Visa | Incorrect CVC | incorrect_cvc |
4000 0000 0000 0119 | Visa | Processing error | processing_error |
4100 0000 0000 0019 | Visa | Blocked as fraudulent | fraudulent |
A decline answers 400 with the reason in code. Any other number answers test_card_required.
3D Secure
A 3D Secure card first answers with a challenge instead of a payment:
{ "status": "REQUIRES_ACTION", "challengeId": "3ds_test_8c1f…", "nextAction": { "type": "sandbox_3ds" } }The test pay page then shows the bank's step, where you choose Complete authentication or Fail authentication. To do the same from code, answer the challenge within 15 minutes; each challenge works once:
/pay/link/{token}/card/3dstest links · /pay/einvoice/{token}/card/3ds for invoices{ "challengeId": "3ds_test_8c1f…", "outcome": "complete" }complete captures the payment (or declines it, for 4000 0084 0000 1629); fail declines with
authentication_failed.
Test payment methods
A test link also offers the other methods the business has turned on in Settings → Online payment methods (iDEAL, Bancontact, Apple Pay, Google Pay, Klarna, …), as simulated payments. On the pay page each one opens a confirmation where you choose Authorize test payment or Fail test payment. From code:
/pay/link/{token}/simulatetest links only{ "method": "ideal", "outcome": "authorize" }An authorized payment is recorded with its method, exactly like a live one (paidMethod: "IDEAL"),
and fires the same test-mode webhooks. fail answers payment_method_failed. Only methods active
for the business are accepted; the list is BaxPay's own copy, refreshed whenever the business opens
its payment-method settings, so a test page never calls a payment processor.
Two hosts, one API
Build against https://sandbox-api.baxpay.com/v1 with a bx_test_… key, then go live on
https://api.baxpay.com/v1 with a bx_live_… key. Paths, payloads and responses are identical, so
going live is two values in your configuration: the base URL and the key.
The key decides the mode: bx_test_… works only on sandbox data, bx_live_… only on live
data. The sandbox host refuses live keys with 403, so a misconfigured server can never move real
money from there. api.baxpay.com still accepts test keys for integrations built before the
sandbox host existed.
Walkthrough: first simulated payment
From a fresh test key to a delivered webhook, in three calls. Grab a bx_test_ key from the
Developers page in the Business dashboard first, and
register a webhook endpoint for payment_session.captured with it.
Create a test connection
The API key's mode decides the connection's mode: a bx_test_ key can only create test
connections. The bxc_ credential is returned once; store it.
Open a session with the credential
This is exactly what your till will do in production: authenticate as the connection and declare the amount.
Simulate the shopper
Approve your own session: the sandbox stand-in for the shopper paying at the terminal.
Receive the webhook
The captured event arrives at your test-mode endpoint, with livemode: false in the payload.
Create the connection with your API key:
curl -X POST https://sandbox-api.baxpay.com/v1/connections \
-H "Authorization: Bearer $BAXPAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "systemType": "ECR", "label": "Sandbox till" }'{
"success": true,
"data": {
"connectionId": "358f1a7a-…",
"credential": "bxc_hwsOcP3Y65z9G1X4…"
}
}Open a session as that connection. Note the switch to the bxc_ credential, the amount in
minor units (1250 = €12.50), and the Idempotency-Key, which is required here:
curl -X POST https://sandbox-api.baxpay.com/v1/integration/sessions \
-H "Authorization: Bearer bxc_hwsOcP3Y65z9G1X4…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"saleReferenceId": "till-01/receipt/42",
"amountMinor": 1250,
"currency": "EUR"
}'{
"success": true,
"data": {
"sessionId": "6d3b3ec5-…",
"saleReferenceId": "till-01/receipt/42",
"livemode": false,
"status": "CREATED",
"type": "SALE",
"amountMinor": 1250,
"capturedAmountMinor": null,
"currency": "EUR",
"resultCode": null,
"final": false
}
}Now play the shopper and approve it:
curl -X POST https://sandbox-api.baxpay.com/v1/integration/sessions/6d3b3ec5-…/simulate \
-H "Authorization: Bearer bxc_hwsOcP3Y65z9G1X4…" \
-H "Content-Type: application/json" \
-d '{ "outcome": "APPROVED" }'{
"success": true,
"data": {
"sessionId": "6d3b3ec5-…",
"status": "CAPTURED",
"capturedAmountMinor": 1250,
"resultCode": "APPROVED",
"authCode": "TEST…",
"maskedInstrument": "•••• 4242 (test)",
"final": true
}
}final: true means the session won't change again. Moments later your test-mode webhook endpoint
receives the event, HMAC-signed like every delivery and explicit about which mode it came from. Its
data is the same session view:
{
"id": "81dab1ba-…",
"type": "payment_session.captured",
"livemode": false,
"data": {
"sessionId": "6d3b3ec5-…",
"saleReferenceId": "till-01/receipt/42",
"status": "CAPTURED",
"amountMinor": 1250,
"capturedAmountMinor": 1250,
"currency": "EUR",
"resultCode": "APPROVED",
"final": true
},
"createdAt": "2026-07-16T14:03:21.530Z"
}Sandbox in the dashboard
Everything above also works without writing code in the Business dashboard. Owners and admins switch a browser tab to sandbox with Enter sandbox in the profile menu, and confirm. That tab then shows test data on every page (transactions, payment links, invoices, clients, products, inventory, analytics, the till) under a yellow SANDBOX bar, and a yellow Developers entry is pinned at the bottom of the sidebar. It lasts until you click Exit sandbox (and confirm), log out or close the tab. Your other tabs, your team and your devices stay live, so your business keeps running normally. Outside sandbox, the developer workspace is under Settings → Developers.
- Pay as a customer: an open test link, or a test invoice's pay link, has Pay with test
card (
4242 4242 4242 4242) and Decline (4000 0000 0000 0002). - Test till: the till sells with test payment links under a TEST banner. Card terminals and cash work only in live: a sandbox tab can never charge a real card.
- Not in sandbox: pages that move real money (banking, transfers, cards, settlements) and, for now, the online shop and accounting say so instead of showing live data. Team and settings are the same in both modes, and say that too.
- In Developers:
- Sessions: test in-store sessions, with Approve, Decline and Time out, the
same outcomes as
/simulate. - Events: every event with each webhook delivery, what your endpoint answered and Resend (event log).
- Requests: every call made with an API key (request log).
- Delete all test data: removes test links, invoices, clients, products, sessions, test payments and events. Test API keys, webhook endpoints and connections stay. Live data is never touched.
- Sessions: test in-store sessions, with Approve, Decline and Time out, the
same outcomes as
Going live
Swap bx_test_ for bx_live_, provision a live connection, and bind it to a real terminal —
nothing else in your code changes. The events you handled in test mode arrive with
livemode: true, and the simulate endpoint stops working, because now there's a real shopper.