Orders
An order is a fiat payout you create. It debits your crypto fund and pays a recipient in local currency. Scan-to-Pay and Cashout create different orders, but they behave identically: same lifecycle, same idempotency rule, same way of tracking.
The sequence
quote → review (optional) → create → poll or webhook
Quote — scan-to-pay ·
cashout. Previews the crypto cost and the fee, and
returns the minAmount / maxAmount this payout accepts. A quote reserves nothing and commits
nothing.
Review — scan-to-pay · cashout. Runs every eligibility check that creation runs — service availability, KYC, currency support, amount bounds, risk screening, spending limits, fund balance — without moving any money. Use it to fail fast and show the user a real error before you commit. Optional, but cheap.
Create — scan-to-pay ·
cashout. Debits your fund and starts the payout. The
response comes back immediately with status PROCESSING; the payout itself is asynchronous.
Track — scan-to-pay · cashout, or a webhook.
Status
| Status | Meaning |
|---|---|
PROCESSING | The order exists and the payout is in flight. Not terminal. |
SUCCESS | The recipient has been paid. Terminal. |
FAILED | The payout did not happen. Terminal, and your fund is refunded. |
Only SUCCESS and FAILED are terminal, and only terminal states fire a webhook.
orderId is your idempotency key
You choose the orderId when you create the order. It must be unique across your partner account,
and reusing one is rejected with DUPLICATE_TRANSACTION_ID (HTTP 409).
That rejection is a feature. If a create request times out, you do not know whether the order was
created — so retry it with the same orderId:
- The retry succeeds → the first attempt never landed, and you now have exactly one order.
- The retry returns
DUPLICATE_TRANSACTION_ID→ the first attempt did land. Fetch the order by its id and carry on.
Either way you cannot double-pay. Generating a fresh orderId on retry is how you double-pay.
async function createOrder(order) {
const {code, data} = await post('/v3/scan-to-pay/orders', order);
if (code === 'SUCCESS') return data;
if (code === 'DUPLICATE_TRANSACTION_ID') return getOrder(order.orderId);
throw new Error(code);
}
Tracking to completion
Register a webhook and you are told when an order reaches a terminal state. Otherwise poll the order endpoint.
The order endpoint is the source of truth in both cases. The callback is delivered once and never retried, so if you miss one — your endpoint was down, the response was slow — fall back to polling rather than assuming the order is still pending.
Paying a merchant QR (Scan-to-Pay)
A scan-to-pay order can target a plain bank account or a merchant QR account.
- Decode the QR your user scanned. No token needed.
- The decoding gives you
bankCode,accountNumber, aqrType, and possibly anadditionalDataobject. - Pass
qrTypeandadditionalDatathrough verbatim when you create the order. Do not build or editadditionalDatayourself — it is opaque.
For a PHP payout with a qrType set, additionalData is required; omitting it rejects the order
with INVALID_PAYMENT_INFO. A plain bank transfer needs neither field.
A note on fiatAmount
fiatAmount is what the recipient receives. The fee is added on top and shown in the quote, and
the crypto debited from your fund covers both. The amount never silently shrinks to absorb a fee.