Sandbox
Drive a payment end-to-end — through matching, completion, failure, expiry, or settlement — without a live counterparty. All sandbox traffic is fully isolated from live data and fires real signed webhooks so you can wire up your handlers before go-live.
How it works
The sandbox is a dedicated slice of the gateway. Every row it writes is pinned is_sandbox = true, and every webhook it delivers carries "sandbox": true on the envelope. Sandbox traffic never touches live escrow, never talks to the matching service, and never reaches a real seller. It is strictly off to the side of production data.
You authenticate with a sandbox API key (prefix tp_sandbox_), obtained from the Merchant Dashboard under Settings → API Keys → Sandbox. Live keys receive a 403 SANDBOX_ONLY on every sandbox endpoint, and sandbox keys receive the same 403 on live endpoints — the two worlds never cross.
Once you POST to /v1/sandbox/payments/simulate with a scenario, the gateway advances the payment through the scripted lifecycle in the background and delivers the matching webhooks to the same URL you have configured for live traffic — signed identically. Your handler does not need to know it is talking to the sandbox.
Base URL
Sandbox requests target a dedicated host that routes only to the sandbox gateway:
https://sandbox-api.tokenpayment.io
Coming soon: the DNS record for sandbox-api.tokenpayment.io is being finalised. Until it is live, point sandbox calls at the main host https://api.tokenpayment.io with a tp_sandbox_ key — the same 403 gate applies and the routing is identical.
Scenario catalog
| Scenario | Terminal status | What it does |
|---|---|---|
| matched_then_completed | completed | Happy path. The payment is created, a synthetic counterparty is matched, funds move through escrow, and the payment settles to `completed`. A `payment.matched` webhook fires, followed by `payment.completed`. |
| matched_then_failed | failed | Failure after match. The payment is created and matched, but the post-match leg fails (e.g. the counterparty drops out). A `payment.matched` webhook fires, followed by `payment.failed` with a populated `failure_reason`. |
| expired | expired | No match within the payment window. The payment is created but never matched. After the simulated window elapses, a single `payment.expired` webhook fires. No funds move. |
| settlement_cycle | settled | Full cycle. Runs the matched-then-completed flow, then additionally advances the payment through a merchant settlement. Fires `payment.matched`, `payment.completed`, and `settlement.completed`. |
| matched_partial_fill | completed | Multi-leg FIFO match. The buy amount is split 65/35 across two synthetic sellers, mirroring the engine’s partial-fill loop. The webhook envelope’s `match_details` array carries both legs with distinct `seller_id`, `match_order`, and `allocated_fiat_amount`. Fires `payment.matched` then `payment.completed`. |
Request shape
scenario is required and must be one of the five tokens above. amount defaults to 10000 minor units and currency defaults to "THB" when omitted — provide them only if your fixture cares.
matched_then_completed
curl https://sandbox-api.tokenpayment.io/v1/sandbox/payments/simulate \
-H "Authorization: Bearer tp_sandbox_..." \
-H "Content-Type: application/json" \
-d '{"scenario": "matched_then_completed"}'matched_then_failed
curl https://sandbox-api.tokenpayment.io/v1/sandbox/payments/simulate \
-H "Authorization: Bearer tp_sandbox_..." \
-H "Content-Type: application/json" \
-d '{"scenario": "matched_then_failed"}'expired
curl https://sandbox-api.tokenpayment.io/v1/sandbox/payments/simulate \
-H "Authorization: Bearer tp_sandbox_..." \
-H "Content-Type: application/json" \
-d '{"scenario": "expired"}'settlement_cycle
curl https://sandbox-api.tokenpayment.io/v1/sandbox/payments/simulate \
-H "Authorization: Bearer tp_sandbox_..." \
-H "Content-Type: application/json" \
-d '{"scenario": "settlement_cycle", "amount": 285000, "currency": "THB"}'matched_partial_fill
curl https://sandbox-api.tokenpayment.io/v1/sandbox/payments/simulate \
-H "Authorization: Bearer tp_sandbox_..." \
-H "Content-Type: application/json" \
-d '{"scenario": "matched_partial_fill", "amount": 157500, "currency": "THB"}'The matched_partial_fill response contains a two-element matches array. Leg #1 absorbs 65% of the buy amount and leg #2 absorbs the remainder, with distinct seller_id values. Use this scenario to verify your webhook listener iterates match_details rather than assuming a singleton match.
Response shape
Every simulate call returns HTTP 201 with the full payment row plus a populated synthetic_match block when the scenario involved matching. The response is the synchronous, terminal view — webhooks follow asynchronously (typically within 100ms on sandbox).
{
"success": true,
"data": {
"scenario": "matched_then_completed",
"payment": {
"id": "pay_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxx",
"merchant_id": "mer_xxxxxxxxxxxxxxxxxxxxxxxxxx",
"order_id": "order_sandbox_001",
"amount": 10000,
"currency": "THB",
"status": "completed",
"is_sandbox": true,
"created_at": "2026-04-20T14:27:00Z",
"matched_at": "2026-04-20T14:27:00Z",
"completed_at": "2026-04-20T14:27:00Z",
"synthetic_match": {
"tokens": {
"chosen_amount": 285,
"rate": 3500,
"match_order": 1,
"status": "active"
}
}
}
}
}Webhook envelope
Sandbox webhooks are byte-identical to live webhooks except for a single additional field on the envelope:
{
"event": "payment.completed",
"payment_id": "pay_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxx",
"order_id": "order_sandbox_001",
"amount": 10000,
"currency": "THB",
"status": "completed",
"sandbox": true,
"idempotency_key": "evt_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxx",
"timestamp": "2026-04-20T14:27:00.123Z"
}The X-TokenPay-Signature header is computed with the same HMAC-SHA256 scheme as live traffic — see Webhooks. Use the sandbox flag to route test traffic to a staging handler if you want, but the signing path is the same either way.
Slip verification (EasySlip) in sandbox
TokenPay verifies Thai bank-transfer slips through EasySlip. Two paths exist for sandbox testing, and they behave differently:
/v1/sandbox/payments/simulatenever invokes EasySlip. The simulate endpoint writes synthetic payment + match rows directly and fires webhooks from in-memory fixtures. No proof is uploaded, no slip image is parsed, no third-party API is called. This is the recommended path for verifying webhook plumbing, signature handling, and merchant-side state machines.- Real proof uploads with a sandbox key DO call live EasySlip. If you create a payment via
POST /v1/paymentswith atp_sandbox_key and then drive a buyer through the checkout widget to upload an actual slip image, the proof-service calls the production EasySlip API with the configured TokenPay API key. This consumes EasySlip quota and requires a valid Thai bank-slip image. Use this path only when you specifically need to exercise slip-parsing UX or proof-side error handling.
EasySlip itself does not currently expose a sandbox tier, so there is no fully-stubbed slip-verification mode today. Track this gap if you need a closed-loop slip test — for now, fixture testing of the proof endpoints is best done at the unit-test layer in proof-service rather than against the live API.
What the sandbox is not
- Not a load-test harness. Sandbox traffic runs on shared infrastructure and is rate-limited.
- Not a perfect mirror of settlement timing. Real settlement cycles run on banking schedules;
settlement_cyclecompletes synchronously for test convenience. - Not a source of truth. Sandbox data may be wiped at any time. Do not rely on sandbox IDs or balances outside of an active test run.
Full schema
The complete request and response schemas for every sandbox endpoint live in the API Reference under the Sandbox tag.