Refunds
Full and partial refunds, where the money comes from, the limits that apply, and how idempotency keeps a refund from being sent twice.
A refund (re_…) returns all or part of a payment's amount to the customer's wallet, in USDC, on the payment's own network. You can refund from the API, the dashboard or the POS terminal; all three share one ledger, so a payment can never be refunded beyond its amount.
Full and partial
// Partial: 1.00 USDC of a 2.50 USDC payment
await zp.refunds.create({ payment: 'pay_1042', amount: 1000000 }, { idempotencyKey: `refund-${returnId}` });
// Full (whatever is left): omit amount
await zp.refunds.create({ payment: 'pay_1042' }, { idempotencyKey: `refund-${returnId}` });- You can refund several times until
amount_refundedequalsamount. The payment moves topartially_refunded, thenrefunded. - Asking for more than what is left answers
400 amount_too_large. A fully refunded payment answers409 payment_already_refunded. - The network fees the customer paid are not refunded. A refund returns at most the sale
amount.
Where the money comes from
Refunds are paid from your merchant balance in the ZyloPay settlement contract (see Settlement). If you have withdrawn the funds, the balance must cover the refund, or the refund fails and nothing moves.
Limits
| Limit | Default | Error |
|---|---|---|
| Refund window after settlement | 90 days | 409 refund_window_expired |
| Refunds per day, per account and network | 10,000 USD | 409 refund_daily_limit_reached |
| One refund in flight per payment | 409 refund_in_progress (retry after it completes) |
Contact ZyloPay to change the window or the daily limit. ZyloPay also screens the recipient; a refused refund answers 403 account_restricted and nothing is sent.
Statuses
The call waits for the transaction receipt, so it usually answers with the final outcome.
| Status | Meaning | What to do |
|---|---|---|
| succeeded | Mined. The funds are with the customer. | Done. |
| pending | Sent, but the receipt was not seen in time. | Do not retry with a new key. It resolves on its own; wait for refund.succeeded or refund.failed. |
| failed | Nothing moved. failure_code is refund_failed or settlement_reverted. | Fix the cause (for example your balance) and create a new refund. |
Idempotency
Idempotency-Key is required. Use a key tied to your own refund record, for example refund-<your return ID>:
- A retry with the same key and body replays the first response, even an error. It never sends a second refund.
- If the first request is still running, the retry answers
409 idempotency_request_in_progress. Wait and retry with the same key. - If you are unsure whether a refund happened, list the payment's refunds (
GET /v1/refunds?payment=pay_…) before trying anything else.
See Handling refunds for the full integration pattern.
Events
refund.created (status pending, before the transaction is sent), then refund.succeeded with payment.refunded, or refund.failed.