Aliquot
Payments

Refunds

Pull refunds through a private claim link — why they are not automatic, how the link reaches the buyer, the network fee, and wrong-token transfers.

Last updated Oct 27, 2026

Any money that reaches a deposit address and is not owed to the seller is held for the buyer in LIABILITY:REFUND_ESCROW:<invoice> and becomes a refund:

reasonWhen
OVERPAYMENTThe invoice is paid with more than its total: the excess (invoice OVERPAID).
EXPIRED_UNDERPAIDA partial payment when the invoice expires (invoice EXPIRED_UNDERPAID, then REFUNDED once paid back).
LATE_PAYMENTA transfer confirmed after the invoice expired, settled or was voided.
WRONG_TOKENAnother canonical token of the same network sent to the address (USDC to a USDT invoice).

Pull, not push

Refunds are never sent back automatically to the address that paid. That address is often an exchange's hot wallet or a contract that cannot receive (or credit) the money. Instead the buyer claims the refund: they open a private link, paste a wallet they control on the payment's network, and the worker sends it there.

<CRYPTO_CHECKOUT_BASE_URL>/pay/<invoice id>/refund/<claim token>. The token (256 bits) is the buyer's only credential, so it is kept out of every place the invoice id travels:

  • The public checkout (GET /api/v1/public/pay/:id, the /pay/:id page) shows that a refund is held, how much, its status and where the link comes from — refund.delivery is email or seller — but never the link.
  • With buyer_email on the invoice, the buyer gets the link by email when the refund becomes claimable (delivery: "email").
  • Otherwise the seller forwards it (delivery: "seller"). The merchant reads it from GET /api/v1/crypto/invoices/:id (refunds[].claim_url), GET /api/v1/crypto/refunds, the refund.claimable webhook (which also carries buyer_email), or the dashboard's Copy claim link button. The dashboard's raw-response view redacts it.

Public refund endpoints are keyed by the claim token only: GET /api/v1/public/refunds/:claimToken and POST /api/v1/public/refunds/:claimToken/claim.

Claiming

POST …/claim { "destination_address": "0x…" } checks the address (valid, EIP-55 checksum when mixed case, not one of the gateway's own addresses, not a contract — a contract answers 422), fixes the network fee and the net amount, and moves the refund CLAIMABLE → PROCESSING (202). Claiming again to the same address is idempotent.

The network fee is the cost of sending the refund, charged to the refund: estimated gas × max fee per gas × CRYPTO_NATIVE_PRICE_USD_<FAMILY> (Polygon 0.50, BSC 600, Base 3000 by default), at least 0.01, booked as REVENUE:REFUND_NETWORK_FEE. The portal shows held, fee and "you receive" before the buyer confirms. When the fee would eat the whole refund (dust), the claim is refused with 422.

The worker sends a refund only after every transfer of that invoice and token has been swept and the sweep is confirmed, then marks it COMPLETED at the network's confirmation threshold (refund.completed webhook). An EXPIRED_UNDERPAID invoice becomes REFUNDED.

Wrong token

A transfer of another canonical token never detects, pays or settles the invoice. Once confirmed it is escrowed in its own token (REFUND_ESCROW:<invoice>:<network>:<token>) with a WRONG_TOKEN refund paid back in that token, whatever the invoice's status. Nothing is credited to any seller in that token. Sweeps run per invoice × token, so the refund waits only for the sweep of its own token. Tokens that are not in the registry are not watched.

[!NOTE] A refund that fails on chain becomes FAILED and its escrow stays owed. There is no retry button yet; it needs an operator.

On this page