Quickstart: your first payment in the sandbox

Five steps, one sitting. By the end you will have moved 42.00 EUR through the sandbox, watched the event land on your own endpoint, and taken part of it back.

  • Reviewed
  • 9 min read
  • API version 2026-04-01

What you need

Three things, none of which requires a sales conversation. The sandbox opens at sign-up and the keys are visible immediately.

  • A BazPay account in test mode. Underwriting is only needed for live keys.
  • Any HTTP client. The samples use curl; the SDKs wrap the same calls.
  • A publicly reachable HTTPS URL for step four. A tunnelling tool is fine in development — the sandbox does not verify who owns the domain.
Before you start

Nothing in this guide moves real money and nothing needs a real card. Test mode is a separate ledger with its own keys, its own webhooks and its own data. You cannot reach live funds with a sk_test_ key even by accident.

Step 1 — Take your test keys

Two keys sit in the dashboard under Developers → API keys. They do different jobs and only one of them is a secret.

The two keys you need in test mode
Key Where it lives What it can do
pk_test_… Browser, in your checkout page Mount hosted fields and tokenise a card. It cannot read or move money.
sk_test_… Server only, in an environment variable Every call in this guide. Treat it like a password; it authorises charges.

The prefix is the mode. A key beginning sk_live_ touches the real ledger, so the safest habit is to read both from configuration and never to type either into a file that gets committed. Rotation, scopes and the webhook secret are covered in authentication and idempotency.

Step 2 — Create a payment

One POST creates the payment object. It is the same call for a card, for iDEAL, for BLIK and for an open banking debit — only method changes. We will use iDEAL because its sandbox flow needs no card details at all.

POST /v1/payments · request
# 42.00 EUR by iDEAL, sandboxcurl -X POST https://api.bazpay.com/v1/payments \  -H "Authorization: Bearer sk_test_9f2c…" \  -H "BazPay-Version: 2026-04-01" \  -H "Idempotency-Key: ord_10482" \  -H "Content-Type: application/json" \  -d '{        "amount": 4200,        "currency": "EUR",        "method": "ideal",        "reference": "ORDER-10482",        "return_url": "https://shop.example/thanks"      }'

amount is in minor units, so 4200 is 42.00 EUR. reference is your own order id and comes back on every event and every settlement line.

The response tells you two things: the id to store, and what has to happen next.

201 Created · response
{  "id": "pay_3fJ2Qk8vTn",  "object": "payment",  "amount": 4200,  "currency": "EUR",  "status": "requires_action",  "method": "ideal",  "reference": "ORDER-10482",  "next_action": {    "type": "redirect",    "url": "https://pay.bazpay.com/r/3fJ2Qk8vTn"  },  "created": "2026-04-12T09:31:07Z"}
Store this now

Persist id against your order before you redirect the payer. It is the only handle you have on the payment afterwards, and the webhook in step four is keyed on it. Do not key your order on the Idempotency-Key — that value expires after 24 hours.

Full request and response fields: API reference · method-by-method behaviour: accepting a payment.

Step 3 — Clear 3-D Secure

A status of requires_action means the payer has to do something before the money can move: approve in a banking app for iDEAL and open banking, or complete 3-D Secure 2 for a card. Send the browser to next_action.url. When it is finished the payer comes back to your return_url with the payment id appended.

Sandbox outcomes on the hosted authentication page
Choose Resulting status Use it to test
Approve succeeded The happy path, and your fulfilment trigger.
Decline failed Your retry copy and the alternate-method prompt.
Abandon canceled The payer closing the tab. Leave the basket intact.
Wait processing Slow bank confirmation. Never assume this one fails.
Do not fulfil on the return URL

The redirect back to your site proves the payer finished the page. It does not prove the money moved, and it can be replayed, bookmarked or simply never reached — a closed laptop ends the browser flow but not the payment. Show a neutral "we are confirming your payment" screen here and let the webhook decide.

Step 4 — Confirm with a webhook

Add one endpoint under Developers → Webhooks and subscribe it to payment.succeeded and payment.failed. The sandbox starts delivering immediately. Within a second or two of the payer approving, this arrives.

POST /hooks/bazpay · delivery
# what lands on your endpoint, headers trimmedPOST /hooks/bazpay HTTP/1.1BazPay-Signature: t=1776072671,v1=6a3f…{  "id": "evt_8Kd1Wm4pQz",  "type": "payment.succeeded",  "created": "2026-04-12T09:31:44Z",  "data": {    "id": "pay_3fJ2Qk8vTn",    "status": "succeeded",    "amount": 4200,    "settles": "sepa_instant"  }}

Verify BazPay-Signature before you read the body. Reply 2xx as soon as you have stored the event; do your fulfilment work afterwards.

A handler that survives production does four things in this order:

  1. Verify the signature against your endpoint's signing secret, and reject anything that fails.
  2. Check the timestamp is within five minutes of now, so an old capture cannot be replayed.
  3. Look up id. If you have seen that event id before, reply 200 and stop.
  4. Reply 2xx within ten seconds. Slow handlers are retried, which is how one order ships twice.

Signature scheme, retry ladder and replay: the webhooks guide.

Step 5 — Refund it

Refunds go back over the rail that took the money, so an iDEAL payment refunds to the same IBAN without you ever handling the account number. Full or partial, the call is the same.

POST /v1/refunds · partial
# partial refund: 12.00 of the 42.00curl -X POST https://api.bazpay.com/v1/refunds \  -H "Authorization: Bearer sk_test_9f2c…" \  -H "Idempotency-Key: rfnd_10482_a" \  -d '{"payment": "pay_3fJ2Qk8vTn", "amount": 1200,       "reason": "requested_by_customer"}'{ "id": "rfnd_5Qw9Lz", "status": "pending",  "amount": 1200, "payment": "pay_3fJ2Qk8vTn" }

Refunds start as pending and settle asynchronously. refund.settled is the event that tells you the payer has the money.

In the sandbox a refund settles in a few seconds. On live traffic a card refund reaches the cardholder in one to five working days depending on the issuer, and a SEPA refund lands the same or next working day. Neither is instant, so your support copy should say "on its way", not "returned".

Where to go next

That is the whole money-in loop. The four pages below cover what the quickstart deliberately simplified, in the order most integrations need them.

  • Authentication Key scopes, version pinning, safe retries and the rate limits you will meet under load.
  • Payments The full status machine, the EU method matrix, SCA exemptions, capture and cancellation.
  • Webhooks Signature verification, the retry ladder, and designing for duplicate and out-of-order events.
  • Testing Cards that force a named decline, forced 3-D Secure outcomes and APM simulators.
  • Go live What underwriting asks for, the technical checks, and what to watch on day one.
If a step did not work

Every response carries a request id in the BazPay-Request-Id header. Send that with your question and support can read the exact exchange rather than asking you to reproduce it. Common first-run stumbles — a live key against a test object, a missing return_url, an unreachable webhook endpoint — all report a specific error code rather than a generic failure.

Keep reading

Stuck on this page?

Integration questions reach an engineer, not a queue. Send the request id and the response body — that is usually enough to answer in one reply.