Skip to main content

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

Quotescan-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.

Reviewscan-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.

Createscan-to-pay · cashout. Debits your fund and starts the payout. The response comes back immediately with status PROCESSING; the payout itself is asynchronous.

Trackscan-to-pay · cashout, or a webhook.

Status

StatusMeaning
PROCESSINGThe order exists and the payout is in flight. Not terminal.
SUCCESSThe recipient has been paid. Terminal.
FAILEDThe 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.

JavaScript
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.

  1. Decode the QR your user scanned. No token needed.
  2. The decoding gives you bankCode, accountNumber, a qrType, and possibly an additionalData object.
  3. Pass qrType and additionalData through verbatim when you create the order. Do not build or edit additionalData yourself — 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.