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.
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.
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.
| 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.
# 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.
{ "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"}
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.
| 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. |
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.
# 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:
- Verify the signature against your endpoint's signing secret, and reject anything that fails.
- Check the timestamp is within five minutes of now, so an old capture cannot be replayed.
- Look up
id. If you have seen that event id before, reply200and stop. - Reply
2xxwithin 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.
# 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.
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.