40 — Developer
Testing
Test mode is a full replica of the live API with simulated providers behind it. Every flow — cards, local methods, declines, even multi-provider failover — can be exercised deterministically before a cent of real money moves.
Test mode vs live mode
The key you authenticate with selects the mode. There is no sandbox URL — https://api.finscale.dev/v1 serves both.
| Test mode | Live mode | |
|---|---|---|
| Key | sk_test_51FinscaleDemo… | sk_live_•••••••••••• |
| Money | Simulated — nothing settles | Real funds, real providers |
| Providers | Simulators with the same ids (prov_eu_acq_01, …) | Your connected providers |
| Objects | "livemode": false | "livemode": true |
| Data | Fully separate; never visible to live keys | Fully separate; never visible to test keys |
Because the two modes share one API surface, promotion to live is a one-line change: swap the key. Webhook endpoints are registered per mode, so add a live endpoint before you flip.
Test cards
In test mode, these card numbers produce fixed outcomes. Any future expiry and any 3-digit CVC work.
| Number | Outcome | What you get |
|---|---|---|
4242 4242 4242 4242 |
succeeds | Payment moves to succeeded immediately after confirmation. |
4000 0000 0000 0002 |
declined | 402 with card_error / card_declined — the generic issuer decline. |
4000 0000 0000 9995 |
declined | 402 with card_error / insufficient_funds. |
4000 0000 0000 3220 |
3DS challenge | Payment returns requires_action with a redirect to a simulated 3-D Secure challenge page; approve or fail it there. |
A declined test card returns the same envelope you'll see in production:
{
"error": {
"type": "card_error",
"code": "insufficient_funds",
"message": "The card has insufficient funds to complete this payment.",
"doc_url": "https://docs.finscale.dev/errors/#insufficient_funds",
"request_id": "req_7Hf3kQd2"
}
}
Simulating local payment methods
Redirect methods get a simulator page instead of the real scheme. Create the payment exactly as in the quickstart; the next_action.url leads to a test approval screen.
iDEAL
With "payment_method": "ideal" in test mode, the redirect shows a simulated bank picker with one entry: Finscale Test Bank. Choosing Approve completes the payment (payment.succeeded webhook follows); Cancel fails it with "code": "payment_canceled".
Blik
Blik authorizes with a 6-digit code from the customer's banking app. In test mode, the simulator page behind next_action.url asks for the code — enter the magic code 777777 to approve:
curl https://api.finscale.dev/v1/payments \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Idempotency-Key: idem_ord_9f21_0716_blik" \
-H "Content-Type: application/json" \
-d '{
"amount": 4900,
"currency": "PLN",
"payment_method": "blik",
"reference": "ord_9f21_0716",
"customer": { "email": "anna@example.com" },
"return_url": "https://shop.example.com/checkout/return"
}'
Any other code declines on the simulator page with "code": "invalid_blik_code".
UPI
With "payment_method": "upi" in test mode, the simulator page behind next_action.url asks for a VPA. Any syntactically valid VPA — anna@testbank, anything@upi — approves the payment; there is no magic VPA to memorize. The magic amount 4999 still forces a first-provider failure first, so you can watch failover on UPI too.
Vouchers
Voucher methods — oxxo, boleto, konbini, multibanco — show a simulated voucher behind next_action.url. In test mode the voucher auto-completes: a few seconds after the voucher page renders, the payment moves to succeeded and the payment.succeeded webhook fires, standing in for the cash payment that takes hours or days in live mode. No store visit, no waiting.
Other redirect methods — Bancontact, PIX, MB Way, Trustly, PayPal, Klarna, and the rest of the catalog — use the same simulator pattern as iDEAL: approve or cancel on the test screen.
Simulating provider failover
The magic amount 4999 forces the first routed provider to fail in test mode (a timeout, in the simulator), so you can watch automatic failover happen — or run it right now in the playground. Create a test instrument from a raw test card first — raw card details are accepted by POST /v1/payment_methods in test mode only:
# 1. Turn a test card into an instrument (test mode only)
curl https://api.finscale.dev/v1/payment_methods \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Content-Type: application/json" \
-d '{
"type": "card",
"card": { "number": "4242424242424242", "exp_month": 12, "exp_year": 2028, "cvc": "123" }
}'
# → { "id": "pm_7Wq2xN9dT4Ls", … }
# 2. Charge it with the magic amount
curl https://api.finscale.dev/v1/payments \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Idempotency-Key: idem_failover_demo_01" \
-H "Content-Type: application/json" \
-d '{
"amount": 4999,
"currency": "EUR",
"payment_method": "card",
"payment_method_id": "pm_7Wq2xN9dT4Ls",
"reference": "ord_9f21_0716"
}'
The response succeeds — on the second provider, with the attempt trail attached:
{
"id": "pay_8Q2mX4nT1cVb",
"object": "payment",
"amount": 4999,
"currency": "EUR",
"status": "succeeded",
"provider": "prov_eu_acq_02",
"provider_attempts": [
{ "provider": "prov_eu_acq_01", "outcome": "timeout", "decline_code": null },
{ "provider": "prov_eu_acq_02", "outcome": "approved", "decline_code": null }
],
"reference": "ord_9f21_0716",
"livemode": false
}
One API call, two attempts, zero extra code — that's the behavior you get for free in production whenever a provider times out, errors, or returns a retryable decline. How Finscale decides which declines are retryable is covered in Errors.
Magic amounts
Four amount values trigger fixed behaviors in test mode, on any payment method:
| Amount | Outcome | What you get |
|---|---|---|
4999 |
failover | The first routed provider times out; the payment succeeds on the second. Both attempts land in provider_attempts. |
4002 |
final decline | The payment fails after routing: status failed with "failure_code": "card_declined" — a final decline that is never retried on another provider. |
4991 |
risk review | Risk screening holds the payment: risk.decision is "review", status stays processing, and a payment.risk_review event fires. No provider is attempted while the review is open. |
4992 |
blocked | Risk screening blocks the payment outright: status failed with "failure_code": "payment_blocked" and risk.decision "blocked". No provider is attempted. |
Every other amount behaves normally. On card payments the risk object is always present with a deterministic score; on redirect methods it is null unless a magic amount forces a risk outcome.
Test webhooks
Webhook endpoints registered in test mode receive events from test payments, signed with your test whsec_FinscaleDemo000… secret — the verification code you write is identical in live mode.
To exercise a handler without writing checkout code, create a cheap test payment (any test card, any amount) and let the real pipeline deliver the event — every delivery is signed and retried exactly as in live mode. To replay an event you already received, use one-click redelivery in the dashboard.
The delivery log (Dashboard → Developers → Webhooks) shows each attempt's response code and body, and offers one-click redelivery. Signature verification and the retry schedule live in Webhooks.
Every simulator behavior on this page — cards, 777777, any-VPA UPI approval, auto-completing vouchers, the magic amounts 4999, 4002, 4991, and 4992 — is stable across accounts and time, so you can pin CI assertions to it.