Introduction

The Zippy Sandbox is a self-service testing tool that emulates the Zippy Pay gateway so you can exercise the integration end-to-end without touching production infrastructure. This guide is a companion to the official Zippy Pay API documentation — it adds sandbox-specific details and ready-to-paste snippets, but never contradicts the production contract. If anything here disagrees with the official docs, the official docs win.

What the sandbox emulates

  • Pay-in and pay-out endpoints with the same validation rules as production.
  • RS256 JWT authentication for pay-out.
  • Asynchronous callbacks to your merchant URLs with the production payload shape.

Sandbox-only extras (not part of production)

  • Manual accept / reject actions from the Transactions UI, where accepting a pending transaction opens a modal that lets you confirm or alter the amount that travels in the callback. Used to drive the integration check flows.
  • "Resend Callback" button on the transaction detail page, used to exercise your idempotency logic without changing any id.

What it does NOT do

  • Move real money or talk to banks, card networks, or PSPs.
  • Persist data longer than 24 hours — old transactions and callbacks are pruned automatically.
  • Enforce rate limits, throttling, or anti-fraud rules present in production.

Use the Integration Check page at any time to see which integration steps your backend has already exercised.

Quick start

  1. Open the Settings page and load your RSA-2048 public key (PEM) and a custom key_callback. The sandbox seeds defaults so you can start testing immediately.
  2. Configure your own callback URLs in Settings — pay-in and pay-out URLs are separate.
  3. Create a pay-in using the snippet below. Copy, paste, send.
  4. Open the Transactions page, select your transaction, and accept or reject it. A callback is dispatched to the URL you configured.
  5. Verify in Callbacks that the payload reached your backend and that your backend responds with 2xx.
Loading...

Integration flows

The full round-trip has four phases.

1. Pay-in

Merchant posts a pay-in request. The sandbox returns a hosted checkout URL. The customer is redirected there; the operator (you, in the sandbox) accepts or rejects it. A callback is sent to callback_url_payin with the MD5-signed payload.

2. Pay-out

Merchant posts a pay-out request with a Bearer JWT signed with the merchant's private key (RS256). The sandbox verifies the JWT using the PEM you uploaded in Settings. The payload must contain at least id (mirroring transactionId) and amount, otherwise the request is refused as manipulated.

3. Transaction status lookup

GET /merchants/:merchantId/transactions/:id returns the current state of a transaction by its merchantRequestId. Protected with MD5 signing — MD5(merchantId + transactionId + key_callback) — in the X-Signature header. The merchantId travels in the URL, so no separate header is needed. Use it as a fallback to the asynchronous callbacks (reconciliation jobs, status refresh after a retry, etc.).

Idempotency & retries

A re-posted pay-in or pay-out request with the same transactionId does not create a second transaction:

  • Pay-in: while the transaction is still pending, the sandbox returns the same zippyId and checkout URL on every retry — safe to retry after a network timeout. Once the transaction is resolved, a further retry is rejected with 403 { status: "error", description: "requires a new transactionId for this payIn" }.
  • Pay-out: per the official docs, any reuse of transactionId is rejected with 403 { status: "error", description: "requires a new transactionId for this payOut" }.
  • Callback resend: use the Resend Callback button on the transaction detail page (or POST /sandbox/transactions/:id/resend-callback) to re-deliver the same callback payload — the MERCHANTREQUESTID and ZIPPYID stay exactly the same, letting you exercise your idempotency logic without mutating any state.

4. Callback verification

Every resolved transaction produces a callback to the URL you configured in Settings. Always verify the SIGN field before trusting the payload — the verification snippet is in the Security section.

The callback AMOUNT may differ from the amount you sent in the original request. Per the official docs, the callback AMOUNT is the final processed amount and is the authoritative value for settlement. Differences can be legitimate (taxes, fees, currency conversion) or indicate tampering. Your backend should: (1) treat the callback AMOUNT as the source of truth for the actual amount processed, and (2) apply your own business policy when it diverges from the requested amount (flag for review, require manual approval, reject, etc.). To simulate this scenario, accept a pending transaction in the sandbox Transactions UI and change the pre-filled amount before confirming — the callback will carry the altered value while the transaction record keeps the original.
Finalization callbacks can arrive more than once for the same transaction, and the final status can change. A transaction that arrived with CODE = 12 (error) may be followed by a later callback with CODE = 0 (approved), and the reverse is also possible (an approved transaction can later be reversed to error). Per the official docs, "transaction statuses may change after the initial callback response." Your backend must: (1) key persistence on MERCHANTREQUESTID (or ZIPPYID) and idempotently accept multiple callbacks for the same transaction, (2) update the stored status to reflect the latest callback rather than locking on the first one, and (3) only trigger final business effects (fulfillment, payout release, refunds) on the authoritative latest state. Use the sandbox Callbacks page to re-send a callback manually and verify your idempotency + state-transition logic.

API reference

POST /pay

Create a pay-in transaction.

Loading...

POST /getPayOutParams

Return the bank list, account types, and document types available for pay-out in a given country. Call it before a pay-out so your UI surfaces valid bankId, typeAccountId, and typeDocumentId values — pay-outs with unknown values are rejected with 400.

Loading...

POST /payOut

Create a pay-out transaction. Requires a Bearer JWT signed with your RSA private key (RS256). The JWT payload must match the request body on id and amount.

Loading...

GET /merchants/:merchantId/transactions/:id

Fetch the current state of a transaction by its merchantRequestId (same value you sent as transactionId). Requires the X-Signature header (MD5 of merchantId + transactionId + key_callback in lowercase hex). The merchantId is taken from the URL path and must match the one assigned to your sandbox instance.

Loading...

Security

Generate an RSA key pair for pay-out

Payout JWTs are signed with RS256. Produce a 4096-bit key pair with OpenSSL:

Loading...
Upload the contents of public.pem in Settings. Never send the private key to the sandbox — keep it only on your backend.

Verify the MD5 SIGN on every callback

The payout callback SIGN is computed as MD5(MERCHANTREQUESTID + AMOUNT + CODE + key_callback) per the official docs; the sandbox applies the same formula to pay-in callbacks as well for symmetry. Compute it on your side and compare before trusting the payload.

Loading...

Troubleshooting

When a sandbox call fails, check these first:

401 on POST /payOut

  • Confirm the Authorization header is present and starts with Bearer (note the trailing space).
  • Confirm the JWT is signed with RS256, not HS256.
  • Confirm the PEM uploaded in Settings matches the private key you used to sign.
  • Confirm the JWT payload has id equal to transactionId and amount equal to the body amount.

401 on GET /merchants/:merchantId/transactions/:id

  • Confirm the URL includes your merchantId as the path segment after /merchants/.
  • Confirm the X-Signature header is present.
  • Confirm you computed the signature with the same key_callback currently active in Settings.
  • Confirm the signature is lowercase hex and uses MD5, not SHA-1.

Callback never reaches your backend

  • Confirm callback_url_payin or callback_url_payout is set in Settings.
  • Inspect Callbacks to see response status and body as received by the sandbox.
  • Ensure your endpoint returns a 2xx — non-2xx is logged but not retried in the sandbox.

Duplicate callbacks for the same transaction

  • This is expected. Zippy Pay can send multiple finalization callbacks for the same MERCHANTREQUESTID, and the final CODE can flip between 0 (approved) and 12 (error) after the initial delivery.
  • Make your callback handler idempotent: persist by MERCHANTREQUESTID (or ZIPPYID) and let later callbacks update the stored status.
  • Gate irreversible business actions (fulfillment, payout release) on the latest authoritative state, not on the first callback received.
  • Use the Callbacks page in the sandbox to re-send a callback manually and validate your idempotency + state-transition logic.

Callback AMOUNT differs from the requested amount

  • This is expected behavior: the callback AMOUNT is the final processed amount (per the official docs), which may legitimately differ from what you requested due to fees, taxes, or currency conversion.
  • Persist the callback AMOUNT as the settled amount; do not overwrite it with your original request value.
  • Decide your own policy for when the divergence exceeds a threshold (flag for review, pause settlement, require manual approval). To simulate it, accept a pending transaction in the Transactions UI and change the pre-filled amount before confirming.

Inspecting a request that the gateway rejected

  • Every request to /pay, /payOut, /getPayOutParams, and /merchants/:merchantId/transactions/:id that returns a 4xx or 5xx response is captured automatically — request headers, body, query, and the response body the gateway returned.
  • Open the Transactions → Failed requests tab to browse them. Click a row to see the full detail.
  • Header values are displayed truncated for readability; use the Copy full button next to each header to copy the untouched value — especially useful for debugging the Authorization (JWT) header or the computed X-Signature.
  • Use the Copy as cURL button in the modal to get a ready-to-run curl command that reproduces the exact request you sent. Paste it in a terminal against the sandbox (or your local build) to iterate on the fix.
  • Captured failures are scoped to your merchant only and are cleaned up together with transactions after 24 hours.