Z Zise Developers 简体中文
Account Center › Guides

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 costsConfigured in each deployment
Upstream servicesCalls 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"}'
behaviorSimulatesExpected observation
successNormal (default)Calls the real upstream
timeoutUpstream returns no bytesOrder has an unknown result; funds remain locked
unknownUpstream 5xx / idempotency middleware failureExactly the same handling as timeout
rejectUpstream definitively rejects the operationOrder fails permanently; frozen funds released
unknown_statusUpstream returns an unrecognized stateOrder 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:

  1. 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.
  2. 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 and paynotify (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.
  3. timeout and unknown share 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.
  4. unknown_status returns the sentinel SANDBOX_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

  1. Obtain a token and verify signature-string construction (the error code is invalid_signature, not 401)
  2. Create a member with POST /v1/members
  3. Report a deposit through POST /v1/deposits—the only endpoint that creates balance without moving existing funds
  4. Use GET /v1/balances to confirm available increased among the eight buckets
  5. Run the simplest service: POST /v1/exchange/quotes → POST /v1/exchange/orders
  6. Configure a webhook endpoint and confirm receipt of exchange.order.executed with successful signature verification
  7. Inject timeout through /v1/sandbox/upstream-behavior and actually exercise your unknown-result branch. Reset to success afterward, or wait one hour for automatic reset
  8. If integrating cards: issue a card, top it up, then use POST /v1/sandbox/cards/{id}/simulate to push card transactions— at minimum, test auth_ok (spending), refund (refund), and decline_insufficient (decline), then send chargeback with repeat: 3 to 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 lookWhat to inspect
GET /v1/cards/{id}/transactionsNew 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 eventRepeated 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:

  1. Each repeat operation uses a new transaction ID. Reusing one would trigger inbox deduplication, and “only recorded once” could be mistaken for ineffective risk controls.
  2. txn_id receives 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.
  3. Insufficient card balance returns insufficient_balance, not a successful authorization. This is correct: the real upstream sends a decline in that situation. Use the decline_insufficient scenario to test that branch.

⚠ Both endpoints also return 404 on live.