ZyloPaydocs

Reconciling payments and payouts

Match every ZyloPay payment and refund to your orders, close each day, and check the balance.

Reconciliation answers three questions: did every order get paid once, does every payment belong to an order, and does the money add up?

Match payments to orders

Each payment carries several keys you can join on:

FieldUse
payment_intentThe intent you created. Store the intent ID with your order when you create it.
order_idYour reference, written on-chain with the settlement. Pass your own when you create the intent (1-8 characters).
terminalWhich terminal took it.
metadata (on the intent)Your own keys, for example { "pos_order": "48213" }.
tx_hashThe Monad transaction, verifiable on the explorer.

For terminal-started payments, order_id is generated by the terminal; match them by terminal and time, or by the intent (source: "terminal").

Record payments as they happen

Listen to payment.succeeded and payment.refunded and upsert your ledger by payment ID. Webhooks are at least once and unordered, so write idempotently and keep the latest amount_refunded (it only grows).

Close a day

Walk each day in UTC with a closed window, after the day has ended:

const window = { created_gte: '2026-09-28T00:00:00Z', created_lt: '2026-09-29T00:00:00Z' };

let gross = 0n;
let fees = 0n;
for await (const p of zp.payments.list({ ...window, limit: 100 })) {
  gross += BigInt(p.amount);
  fees += BigInt(p.fees.acquirer + p.fees.protocol + p.fees.cashback);
  await ledger.upsertPayment(p); // idempotent
}

let refunded = 0n;
for await (const r of zp.refunds.list({ ...window, limit: 100 })) {
  if (r.status === 'succeeded') refunded += BigInt(r.amount);
  await ledger.upsertRefund(r);
}
  • A payment's created is when ZyloPay indexed it, about a second after settled_at (the block time). Filter on created; use settled_at for reporting if your books use chain time.
  • A refund can be pending for a short while. Re-read pending refunds before you close, or close the day once none is pending.
  • Refunds belong to the day they were made, not the day of the payment.
  • fees are paid by the customer, not by you. net equals amount.

Check the balance

Your balance in the settlement contract moves like this:

balance(end) = balance(start) + payments.amount − succeeded refunds − withdrawals

Read it with GET /v1/balance (live from the chain, with as_of). Withdrawals are made in the dashboard and are not in the API today; take them from the dashboard's withdrawal history or from the settlement contract's FundsWithdrawn events for your merchant. Record the balance at the same time each day to compare.

Verify on-chain

For audit, every payment can be verified independently: look up tx_hash on the Monad explorer and check the settlement contract's event for your merchant, amount and order_id. See Settlement.

Common mismatches

SymptomLikely cause
Order paid twiceTwo intents for one order. Use an idempotency key derived from the order, and cancel the old intent before creating a new one.
Payment without an orderA sale started on the terminal. Match by terminal and time, or train staff to enter the order reference.
Order unpaid but a payment existsA missed webhook. Run the daily walk, or catch up from GET /v1/events.
Totals off by feesYou summed total_charged instead of amount. You receive amount.

On this page