Getting started

Sandbox and test scenarios

Test mode runs the full orchestration path — risk, routing, attempts, failover, webhooks, ledger and settlement — against NATIO demo providers. You choose the outcome of each payment, so every branch of your integration is reachable on demand.

Same contract, simulated providers
The request and response shapes, the status machine, the error envelope and the webhook signatures are identical in test and live mode. Only the providers behind the orchestration layer change. No funds move in test mode and no card data is involved at any point.

Demo providers

A sandbox account is seeded with three demo provider accounts and a set of routing rules that use them. They are ordinary provider accounts as far as the orchestration engine is concerned: they have priorities, fees, limits and simulated latency, and they appear in routing decisions like any other.

ProviderCodeTypeRole in the sandbox
NATIO Demo Acquirer Ademo_acquirer_aAcquirerFirst choice for card traffic in the seeded routing rules. Simulates a card acquirer with an amount limit.
NATIO Demo Acquirer Bdemo_acquirer_bAcquirerFallback for card traffic. Used to demonstrate failover when Acquirer A fails technically.
NATIO Demo QR Providerdemo_qrQR / localAsynchronous rail. Returns next_action of type qr_code and completes through the sandbox hosted page.

The seeded card rule orders Acquirer A before Acquirer B, which is what makes a failover visible: the first attempt goes to A, and a retryable failure cascades to B inside the same request. Routing rules are editable per merchant, so your own sandbox can be shaped to match the production topology you intend to run.

Test scenarios

A scenario is expanded by the orchestrator into a per-attempt outcome hint for the demo adapters. That is why a scenario can describe behaviour across several providers: failover makes the first attempt fail technically and the second succeed.

test_scenarioBehaviour
successPayment is approved by the first provider.
authorizePayment is authorised only; capture it with POST /v1/payments/{id}/capture.
requires_actionProvider requests customer action (hosted page / redirect). Complete it via the sandbox hosted page.
soft_declineFirst provider soft-declines (insufficient funds); NATIO cascades to the next provider.
hard_declineProvider hard-declines (stolen card). No retry; payment fails.
technical_errorFirst provider returns a technical error; NATIO fails over to the next provider.
failoverAlias of technical_error: Provider A fails technically, Provider B succeeds.
timeoutFirst provider times out, NATIO confirms no charge exists and fails over.
timeout_recoveredProvider times out but had processed the payment; NATIO recovers it via status lookup — no double charge.
unavailableFirst provider is unavailable (HTTP 503); NATIO fails over.
all_failEvery eligible provider fails technically; payment ends as failed.
reviewRisk engine returns REVIEW; payment waits for manual approval in admin.
blockRisk engine returns BLOCK; payment fails immediately.

How to trigger a scenario

Send the scenario name in the test_scenario field on POST /v1/payments. No other part of the request changes.

curl
curl https://api.natio.me/v1/payments \
  -H "Authorization: Bearer natio_sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10000,
    "currency": "USD",
    "payment_method": "card",
    "test_scenario": "soft_decline"
  }'

The value is echoed back on the payment object as test_scenario, and the simulated primitive for each attempt appears in the timeline on the provider.request_sent event (for example sandbox: technical_error), so it is always clear which branch ran.

Refunds accept a smaller set on POST /v1/payments/{id}/refund: success, technical_error, hard_decline.

The catalogue is also readable from the API, so a test harness never has to hardcode it:

GET /v1/test/scenarios — test keys only
curl https://api.natio.me/v1/test/scenarios \
  -H "Authorization: Bearer natio_sk_test_..."

{
  "data": [
    { "name": "success", "description": "Payment is approved by the first provider." },
    { "name": "authorize", "description": "Payment is authorised only; capture it with POST /v1/payments/{id}/capture." }
  ]
}
Test mode only
test_scenario is rejected outright with a live key — it is never silently ignored. Strip the field in your production code path rather than relying on the environment.
400 Bad Request — live key
{
  "error": {
    "type": "invalid_request_error",
    "code": "test_scenario_not_allowed",
    "message": "test_scenario is only accepted with test API keys",
    "param": "test_scenario"
  },
  "request_id": "req_0uwP4ioIZ8q8dgBW"
}

Hosted-page simulation

Redirect and QR rails do not resolve inside the create request. With test_scenario: "requires_action" the payment comes back as processing and carries a next_action object pointing at a sandbox hosted page that stands in for the provider one.

201 Created — requires_action
{
  "id": "pay_9yLEFy41uUGI0CMhbswo",
  "status": "processing",
  "next_action": {
    "url": "http://localhost:4000/sandbox/hosted/pa_lj6mWPXP7g7EAFN9QlC9/mp_mock_acquirer_tTa8hRyaVFXNdq",
    "type": "redirect",
    "expiresAt": "2026-09-22T02:47:58.149Z"
  },
  "route": {
    "provider": { "id": "prv_mKJKu0bhvoQO3EMPKTzt", "code": "demo_acquirer_a", "name": "NATIO Demo Acquirer A" },
    "provider_account_id": "pa_lj6mWPXP7g7EAFN9QlC9",
    "provider_payment_id": "mp_mock_acquirer_tTa8hRyaVFXNdq",
    "attempts": 1,
    "rule": "Cards → Acquirer A, fallback Acquirer B"
  }
}

Open next_action.url in a browser. The page is a simulation of a provider checkout: it shows the reference and amount, and offers approve or decline. Choosing one makes the demo provider send a signed notification back to NATIO exactly as a real provider would, which moves the payment to successful or failed and emits the corresponding webhook event. Nothing is polled by your code: the transition arrives as an event.

The QR provider returns the same flow with a qr_code action instead. qrPayload is the string you would render as a QR image for the customer; url is the simulation page that completes it.

next_action for a QR rail
{
  "type": "qr_code",
  "qrPayload": "NATIO-DEMO-QR|mp_mock_qr_...|10000|VND",
  "url": "https://api.natio.me/sandbox/hosted/pa_.../mp_mock_qr_...",
  "expiresAt": "2026-09-22T03:02:11.402Z"
}
FieldPresent onMeaning
typeallOne of redirect, qr_code, display_details.
urlredirect, qr_codeWhere the customer completes the payment. In the sandbox this is the simulation page.
qrPayloadqr_codeThe payload to encode into a QR image.
expiresAtallAfter this instant the action can no longer be completed and the payment expires.
Return URL
Send return_url on the create request to control where the customer lands after the action. Treat that redirect as a navigation hint only — the payment status is whatever the API and the webhook say it is, never what a query parameter on the return URL claims.

Sandbox provider report (CSV)

Reconciliation is only meaningful if there is a second source of truth to compare against. Each demo provider account keeps its own ledger and can export it as a CSV in the same shape a real provider delivers a transaction or settlement report, so the whole reconciliation flow can be exercised end to end.

Download it from Reconciliation in the dashboard, per demo provider account, in test mode. Upload it back on the same page to run a reconciliation batch.

provider-report-pa_….csv
provider_reference,natio_reference,type,amount,currency,status
mp_mock_acquirer_2F5S5ySxFfbcpW,pay_GhIQ6vl0fEbcIpC2VsQz,payment,5000,EUR,settled
mp_mock_acquirer_jzQP8MGRTSYTsZ,pay_3dIYLVfvZh2jXf34PKEK,payment,2000,GBP,settled
ColumnMeaning
provider_referenceThe provider side identifier of the movement. This is the primary key of the match.
natio_referenceThe NATIO object the provider recorded against the movement, when it carried one. Used as the fallback match.
typeThe kind of movement: payment, refund or payout.
amountInteger in the minor unit of the currency, like every amount in the API.
currencyISO 4217 code.
statusThe status as the provider reports it. Pending and authorised records are not exported; only movements that reached a final state are.

A batch classifies every line as matched, missing on the NATIO side, missing on the provider side, an amount mismatch or a status mismatch. Uploading the demo CSV unchanged should produce a clean match; editing an amount or deleting a line is the fastest way to see how each exception is reported before you wire up a real provider feed.

Suggested coverage

A sandbox run that exercises these scenarios covers every branch most integrations need:

What you are provingScenario
The happy path and the ledger line it producessuccess
Manual capture, and cancelling an authorisation insteadauthorize
Redirect or QR handling, and the event that resolves itrequires_action
Soft declines cascading to another providersoft_decline
Hard declines shown to the customer without a retryhard_decline
Technical failover between providers inside one requestfailover
Timeouts, including the no-double-charge recovery pathtimeout, timeout_recovered
A provider being down entirelyunavailable
Your handling of a terminal failure after every provider is exhaustedall_fail
Payments held for manual review, and payments refused by riskreview, block