Skip to content
BaxPayDevelopers

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 keysprefix

Secret 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 tab

The 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-scoped

Webhook 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:

POST/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.

NumberBrandResultcode
4242 4242 4242 4242VisaSucceeds
5555 5555 5555 4444MastercardSucceeds
3782 822463 10005American ExpressSucceeds
4000 0025 0000 1001Cartes BancairesSucceeds
4000 0025 0000 3155Visa3D Secure, then succeeds
4000 0000 0000 3220Visa3D Secure 2, then succeeds
4000 0084 0000 1629Visa3D Secure, then declinedcard_declined
4000 0000 0000 0002VisaDeclinedcard_declined
4000 0000 0000 9995VisaInsufficient fundsinsufficient_funds
4000 0000 0000 9987VisaLost cardlost_card
4000 0000 0000 9979VisaStolen cardstolen_card
4000 0000 0000 0069VisaExpired cardexpired_card
4000 0000 0000 0127VisaIncorrect CVCincorrect_cvc
4000 0000 0000 0119VisaProcessing errorprocessing_error
4100 0000 0000 0019VisaBlocked as fraudulentfraudulent

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:

POST/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:

POST/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.

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.