ZyloPaydocs

Quickstart

Get test keys, call the API, receive a verified webhook, create a payment intent and take a sandbox payment. About 15 minutes.

This guide uses test mode. Test mode runs on the Monad testnet with test USDC. Nothing in it moves real money. Every step works the same way in live mode with a live key.

You need:

  • a ZyloPay merchant account (merchant.zylopay.com), with the owner or admin role;
  • Node.js 18 or later, and curl;
  • for the last two steps, a ZyloPay POS terminal (Android: Multzo H10, Clover or a generic NFC device) and a phone with Apple Wallet or Google Wallet.

1. Get your test keys

Sign in to the dashboard. Switch to Sandbox (the network switch in Settings; a yellow banner confirms it). Test keys, test webhook endpoints and testnet terminals are only visible in Sandbox.

If the dashboard offers Go Live, press it. This registers your merchant on the testnet contract. Without it, the API answers 409 account_not_live to payment intents and lists no payments. Testnet needs no business verification.

Open Developers → API keys → Create key. Pick Test mode and Secret type. Keep all scopes for now (you will narrow them before going live). Copy the key: it is shown once.

Store the key in an environment variable. Never commit it.

export ZYLOPAY_SECRET_KEY="zp_test_sk_…"

Check it with a first request:

curl https://zylopay-api.fly.dev/api/v1/balance \
  -H "Authorization: Bearer $ZYLOPAY_SECRET_KEY"

livemode: false confirms test mode. The key chooses the network: you never send a network header. A 401 means the key is missing, mistyped, expired or revoked (see Authentication).

SDK status

@zylopay/node is pre-release. Until it is published on npm, use the curl examples or call the API with fetch. See Node SDK.

2. Receive and verify a webhook

ZyloPay delivers events by POST to a public URL. In test mode the URL may use http, but it must be reachable from the internet: private, loopback and link-local addresses (localhost, 10.x, 192.168.x) are refused. To receive events on your laptop, expose a local port with a tunnel such as cloudflared tunnel --url http://localhost:4242 or ngrok http 4242.

Create a small receiver. It reads the raw body, verifies the ZyloPay-Signature header and prints the event.

server.ts
import http from 'node:http';
import ZyloPay from '@zylopay/node';

const zp = new ZyloPay(process.env.ZYLOPAY_SECRET_KEY!);
const secret = process.env.ZYLOPAY_WEBHOOK_SECRET!; // whsec_…

http
  .createServer(async (req, res) => {
    const chunks: Buffer[] = [];
    for await (const chunk of req) chunks.push(chunk as Buffer);
    const rawBody = Buffer.concat(chunks).toString('utf8');
    try {
      const event = zp.webhooks.constructEvent(rawBody, req.headers['zylopay-signature'] as string, secret);
      console.log(event.type, event.id);
      res.writeHead(200).end();
    } catch (err) {
      res.writeHead(400).end(); // bad signature or stale timestamp
    }
  })
  .listen(4242);

Register the endpoint with your tunnel URL. The response carries the signing secret once.

curl https://zylopay-api.fly.dev/api/v1/webhook_endpoints \
  -H "Authorization: Bearer $ZYLOPAY_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://YOUR-TUNNEL.example/", "enabled_events": ["*"], "description": "Quickstart"}'

Start the receiver with the secret, then send a test event:

export ZYLOPAY_WEBHOOK_SECRET="whsec_…"
npx tsx server.ts   # or: node server.mjs

curl -X POST https://zylopay-api.fly.dev/api/v1/webhook_endpoints/we_…/test \
  -H "Authorization: Bearer $ZYLOPAY_SECRET_KEY"

Your receiver prints ping evt_…. The response of the test call is the delivery, with status: "succeeded" and your endpoint's HTTP status. You can also press Send test on the endpoint in Developers → Webhooks.

3. Provision a testnet terminal

In the dashboard (still in Sandbox), open Settings → POS Terminals → Add Terminal. The terminal key is shown once, with a QR code. Install the ZyloPay POS app on the device (ask your ZyloPay contact for the build), choose testnet on its setup screen and scan the QR code. A terminal key works only on the network it was created on.

List your terminals to get the terminal ID:

curl https://zylopay-api.fly.dev/api/v1/terminals \
  -H "Authorization: Bearer $ZYLOPAY_SECRET_KEY"
{ "object": "list", "data": [{ "id": "tml_ZP-4821-K", "status": "active", "network": "testnet", "…": "…" }], "has_more": false, "next_cursor": null }

Your receiver also got terminal.created.

4. Create a payment intent

A payment intent asks one terminal to take one payment. amount is in USDC atoms: 6 decimals, so 2500000 is 2.50 USDC. Idempotency-Key is required: a retry with the same key never creates a second intent.

curl https://zylopay-api.fly.dev/api/v1/payment_intents \
  -H "Authorization: Bearer $ZYLOPAY_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"amount": 2500000, "terminal": "tml_ZP-4821-K", "description": "Quickstart"}'

The intent is requires_payment_method: it waits for a tap until expires_at (120 seconds by default, expires_in 30-600). Your receiver prints payment_intent.created.

What happens next, honestly

A terminal collects an API-created intent by polling ZyloPay with its terminal key, shows the amount and settles it when the customer taps. The ZyloPay POS app picks up intents from the release that adds intent pickup, which is rolling out now: confirm with support that your terminals run it. A terminal that does not pick the intent up leaves it requires_payment_method until it is canceled at expires_at. Cancel it now to see the event:

curl -X POST https://zylopay-api.fly.dev/api/v1/payment_intents/pi_…/cancel \
  -H "Authorization: Bearer $ZYLOPAY_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Your receiver prints payment_intent.canceled. The next step takes a real sandbox payment started on the terminal.

5. Take a sandbox payment

A sandbox payment needs a testnet pass with test USDC behind it.

Create a testnet pass. On the phone, open the payer app at pay.zylopay.com, sign in, and switch its network to testnet in Settings. Get testnet MON for gas from the Monad faucet (the app links it), and put test USDC in the wallet. ZyloPay's testnet USDC is 0x534b2f3A21130d7a60830c2Df862319e593943A3; if you have no source for it, ask your ZyloPay contact. Approve USDC once in the app, then add the pass to Apple Wallet or Google Wallet.

Charge on the terminal. Enter an amount on the POS app and let the customer tap the pass. On testnet, a terminal without a testnet Smart Tap key takes the payment by scanning the pass's QR code instead; the result is the same.

Observe it. Within about a second your receiver prints payment_intent.succeeded (a terminal-started payment is an intent with source: "terminal") and payment.succeeded. List it:

curl "https://zylopay-api.fly.dev/api/v1/payments?limit=1" \
  -H "Authorization: Bearer $ZYLOPAY_SECRET_KEY"

A payment can be held for the customer's confirmation in the payer app (requires_action), for example their first payment at your store when ZyloPay's risk policy asks for it (reason FIRST_MERCHANT_PAYMENT). Confirm it on the phone within 60 seconds. See Payer confirmation.

6. Refund it

curl https://zylopay-api.fly.dev/api/v1/refunds \
  -H "Authorization: Bearer $ZYLOPAY_SECRET_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"payment": "pay_…", "amount": 1000000}'

The refund answers succeeded once mined (or pending if the confirmation was not seen in time: it resolves on its own). You receive refund.created, refund.succeeded and payment.refunded.

Next steps

On this page