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.
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.
| Provider | Code | Type | Role in the sandbox |
|---|---|---|---|
| NATIO Demo Acquirer A | demo_acquirer_a | Acquirer | First choice for card traffic in the seeded routing rules. Simulates a card acquirer with an amount limit. |
| NATIO Demo Acquirer B | demo_acquirer_b | Acquirer | Fallback for card traffic. Used to demonstrate failover when Acquirer A fails technically. |
| NATIO Demo QR Provider | demo_qr | QR / local | Asynchronous 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_scenario | Behaviour |
|---|---|
success | Payment is approved by the first provider. |
authorize | Payment is authorised only; capture it with POST /v1/payments/{id}/capture. |
requires_action | Provider requests customer action (hosted page / redirect). Complete it via the sandbox hosted page. |
soft_decline | First provider soft-declines (insufficient funds); NATIO cascades to the next provider. |
hard_decline | Provider hard-declines (stolen card). No retry; payment fails. |
technical_error | First provider returns a technical error; NATIO fails over to the next provider. |
failover | Alias of technical_error: Provider A fails technically, Provider B succeeds. |
timeout | First provider times out, NATIO confirms no charge exists and fails over. |
timeout_recovered | Provider times out but had processed the payment; NATIO recovers it via status lookup — no double charge. |
unavailable | First provider is unavailable (HTTP 503); NATIO fails over. |
all_fail | Every eligible provider fails technically; payment ends as failed. |
review | Risk engine returns REVIEW; payment waits for manual approval in admin. |
block | Risk 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 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:
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_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.{
"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.
{
"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.
{
"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"
}| Field | Present on | Meaning |
|---|---|---|
type | all | One of redirect, qr_code, display_details. |
url | redirect, qr_code | Where the customer completes the payment. In the sandbox this is the simulation page. |
qrPayload | qr_code | The payload to encode into a QR image. |
expiresAt | all | After this instant the action can no longer be completed and the payment expires. |
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_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| Column | Meaning |
|---|---|
provider_reference | The provider side identifier of the movement. This is the primary key of the match. |
natio_reference | The NATIO object the provider recorded against the movement, when it carried one. Used as the fallback match. |
type | The kind of movement: payment, refund or payout. |
amount | Integer in the minor unit of the currency, like every amount in the API. |
currency | ISO 4217 code. |
status | The 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 proving | Scenario |
|---|---|
| The happy path and the ledger line it produces | success |
| Manual capture, and cancelling an authorisation instead | authorize |
| Redirect or QR handling, and the event that resolves it | requires_action |
| Soft declines cascading to another provider | soft_decline |
| Hard declines shown to the customer without a retry | hard_decline |
| Technical failover between providers inside one request | failover |
| Timeouts, including the no-double-charge recovery path | timeout, timeout_recovered |
| A provider being down entirely | unavailable |
| Your handling of a terminal failure after every provider is exhausted | all_fail |
| Payments held for manual review, and payments refused by risk | review, block |