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 modeLive mode
Keysk_test_51FinscaleDemo…sk_live_••••••••••••
MoneySimulated — nothing settlesReal funds, real providers
ProvidersSimulators with the same ids (prov_eu_acq_01, …)Your connected providers
Objects"livemode": false"livemode": true
DataFully separate; never visible to live keysFully 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.

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

402 Payment Required
{
  "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:

POST /v1/payments — Blik, approved
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 VPAanna@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:

POST /v1/payments — amount 4999 forces failover
# 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:

201 Created — routed to prov_eu_acq_02
{
  "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:

AmountOutcomeWhat 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.

Deterministic by design

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.