zinfra.dev is an independent test environment with its own balances, members, and orders.
Sandbox
The sandbox is the zinfra.dev test deployment, with a database independent of the zinfra.vip production deployment.
https://api.zinfra.dev
API Keys have no sandbox/live type. Create credentials in the merchant portal of the corresponding environment. The test environment does not enforce IP allowlists; production requires one.
What Is Isolated
| Isolation | |
|---|---|
| Members, orders, ledger | ✅ Separate databases; production cannot query test members |
| Webhook deliveries | ✅ Webhook URLs configured separately per environment |
| Service switches and three-part costs | Configured in each deployment |
| Upstream services | Calls real upstreams, with optional fault injection (see below) |
Upstream Fault Injection
The sandbox runs business code identical byte for byte to production, so every order calls the real upstream by default. A real upstream will not time out on demand, yet upstream timeouts are among the most important and error-prone cases to test.
/v1/sandbox/upstream-behavior lets you preset an upstream to a specified outcome:
curl -X POST https://api.zinfra.dev/v1/sandbox/upstream-behavior \
-H 'x-auth-token: Bearer <沙盒令牌>' \
-d '{"upstream":"qrpay","behavior":"timeout"}'
| behavior | Simulates | Expected observation |
|---|---|---|
success | Normal (default) | Calls the real upstream |
timeout | Upstream returns no bytes | Order has an unknown result; funds remain locked |
unknown | Upstream 5xx / idempotency middleware failure | Exactly the same handling as timeout |
reject | Upstream definitively rejects the operation | Order fails permanently; frozen funds released |
unknown_status | Upstream returns an unrecognized state | Order is suspended and triggers our alert |
upstream accepts remittance (remittance), card (cards), qrpay (QR payments), or chain (on-chain deposits and withdrawals). These are business lines, not particular providers—which providers we integrate, how many, and when they change are outside the contract, so your integration scripts do not break when we change providers. Injection automatically returns to success after 1 hour; forgetting to reset it would otherwise cause unexplained failures in your next integration session.
Four essential points:
- Injection replaces an upstream response, not an order state. We process it using exactly the same classification, retries, audit trail, and state machine as production. The order states and webhooks you see in the sandbox therefore represent what the same situation would produce in production.
- Only the money-moving step is replaced. Read-only quote, decode, and query calls to that upstream still go to the real service. This is intentional: a QR payment calls
createpay(quote) first andpaynotify(move funds) afterward. Replacing every call would fail cleanly before locking funds, never reaching the branch you need to test: funds may already have been released. timeoutandunknownshare the same handling; this is not redundant. They look very different in your logs (no response versus a 5xx), but must be handled identically: both have an unknown result, and neither may be treated as failure and refunded. Separate cases let you verify exactly that.unknown_statusreturns the sentinelSANDBOX_UNKNOWN_STATUS. It simulates an upstream silently adding an enum value. Our rule is to suspend and alert, never default to “processing.” Your state machine should follow the same rule.
⚠ Both endpoints return 404 on live (not 403): a route that changes upstream behavior by request must not exist in production. Calling it with a Live Key has no effect.
The Most Important Cases to Inject
timeout / unknown. These produce an unknown result: the request may have reached the upstream and funds may already have been paid, with only the receipt missing. Refunding or unfreezing as if it failed means both refunding and paying.
This path may not occur even once a year in a real environment, so it is often the only integration branch that reaches production untested. Exercise it in the sandbox at least once.
One-Click Reset
POST /v1/sandbox/reset clears sandbox call and delivery logs.
⚠ It does not seed fixtures. After resetting, create members and report deposits yourself. Do not expect a ready-made test dataset.
Recommended Integration Sequence
- Obtain a token and verify signature-string construction (the error code is
invalid_signature, not 401) - Create a member with
POST /v1/members - Report a deposit through
POST /v1/deposits—the only endpoint that creates balance without moving existing funds - Use
GET /v1/balancesto confirmavailableincreased among the eight buckets - Run the simplest service:
POST /v1/exchange/quotes→POST /v1/exchange/orders - Configure a webhook endpoint and confirm receipt of
exchange.order.executedwith successful signature verification - Inject
timeoutthrough/v1/sandbox/upstream-behaviorand actually exercise your unknown-result branch. Reset tosuccessafterward, or wait one hour for automatic reset - If integrating cards: issue a card, top it up, then use
POST /v1/sandbox/cards/{id}/simulateto push card transactions— at minimum, testauth_ok(spending),refund(refund), anddecline_insufficient(decline), then sendchargebackwithrepeat: 3to freeze the card
Do not skip step 6. Signature failures are among the most common integration support issues and are much easier to diagnose in your environment than from ours.
Do not skip step 7 either. It is the only step simulating adverse conditions; the first six test the happy path, while the few minutes when things go wrong are what can lose money.
Card Transactions: The Upstream-to-Platform Half
Step 8 deserves separate explanation because it differs from the first seven.
Upstream fault injection replaces our outbound call. Card spending, refunds, reversals, settlements, and declines are pushed to us by the upstream. There is no outbound call in this path, so injection is a no-op here. Sandbox cards also cannot generate real purchases without an acquiring side. Consequently, your entire card transaction handling code could reach launch without executing a single line.
# 先看有哪些场景,每一档预期会发生什么
curl https://api.zinfra.dev/v1/sandbox/card-scenarios \
-H 'x-auth-token: Bearer <沙盒令牌>'
# 推一笔 12.99 的消费过来
curl -X POST https://api.zinfra.dev/v1/sandbox/cards/crd_<卡 id>/simulate \
-H 'x-auth-token: Bearer <沙盒令牌>' \
-H 'x-on-behalf-of: <你的会员号>' \
-H 'x-idempotency-key: <uuid>' \
-d '{"scenario":"auth_ok","amount":"12.99","merchant_name":"NETFLIX.COM"}'
The payload passes through the exact production pipeline (inbox deduplication → normalization → direction classification → posting only on succeed → ledger → limits → risk counts → penalties/card freezing). Only signature verification is skipped.
⚠ Webhooks are not where you observe the result. Card spending does not emit public events— one event per purchase would turn the event stream into a call-volume amplifier, and an ID-and-state-only payload would still require a query. Check these three places:
| Where to look | What to inspect |
|---|---|
GET /v1/cards/{id}/transactions | New transaction rows. Also read direction—amount is absolute, so a refund and purchase look identical if you inspect only the amount |
GET /v1/cards/{id} | Changes in balance and limits |
card.status.updated event | Repeated chargeback calls crossing the freezing threshold really emit this event. It is the only event pushed to you on this path |
auth_code in the response identifies this operation in transaction history.
Three essential points:
- Each
repeatoperation uses a new transaction ID. Reusing one would trigger inbox deduplication, and “only recorded once” could be mistaken for ineffective risk controls. txn_idreceives a merchant prefix before use. The transaction deduplication index is globally unique; unrestricted IDs would let you occupy someone else's ID, silently causing their transaction to be considered a duplicate.- Insufficient card balance returns
insufficient_balance, not a successful authorization. This is correct: the real upstream sends a decline in that situation. Use thedecline_insufficientscenario to test that branch.
⚠ Both endpoints also return 404 on live.