Scan-to-Pay
Your user scans a merchant's QR code. You decode it, quote what it will cost in crypto, and create an order. AliX then debits your fund and pays the merchant in local currency.
The flow:
- Decode the QR —
get-qr-code-info. Public, no signature. - Quote it —
review-sell-order. - Create the order —
create-sell-order, passing the decoded QR data through inextendInfo. - Track it —
orders/details, plus a webhook.
The user must be KYC-verified first. See KYC.
The order object
create-sell-order, orders/details and the webhook all return the
same object, and so does cashout.
| Field | Type | Notes |
|---|---|---|
externalOrderId | string | Your id for the order. |
type | string | SELL for scan-to-pay, not SCAN_TO_PAY. CASHOUT for a cashout. |
fiatAmount | number | The fee-inclusive total you are charged, not the amount the merchant receives. |
paidAmount | number | |
status | string | See below. |
descriptions | string | Plural. Success, or the failure reason. |
createdAt | string | 2026-07-14 10:32:07 — UTC+7, no zone marker. |
expiresAt | string | Created + 15 minutes, same format. |
tokenTransfer | object | currency, network, price, amount, address, txHash, memo. |
bankTransfer | object | bankAccountName, bankAccountNumber, bankName, bankCode, contentPayment, totalPayment, qrUrl. |
fees | object | systemFee, processingFee. |
signature | string | Over externalOrderId|type|fiatAmount|status|secretKey. |
bankTransfer also carries an expiresAt of its own, which arrives empty; read expiry from the
order-level expiresAt.
Status
An order starts at AWAITING_PAYMENT and settles at SUCCESS or ERROR — the two terminal
states, and the two that fire the webhook. The states between mark
the payout in flight.
| Status | Meaning |
|---|---|
AWAITING_PAYMENT | Created; the payout has not gone out yet. |
PAYMENT_COMPLETED | The recipient has been paid. |
PROCESSING | In flight. |
SUCCESS | Done. Terminal. |
ERROR | Failed. Terminal. Your fund has been refunded. |
Treat a status you do not recognise as still in progress.
Decode a QR code
GET /api/v2/public/get-qr-code-info?qrContent=<raw QR string>
Public: no signature, no partnerCode. Pass the raw string your scanner read.
Response data
| Field | Type | Notes |
|---|---|---|
bankCode, bankName | string | Feed bankCode straight into create-sell-order. |
bankAccountNumber | string | Likewise. |
recipientName | string | |
countryCode | string | VN, PH, TH, GE, BR, AR, PE. |
amount | number | Present only if the QR fixes an amount. |
content | string | |
qrType | string | vietqr, ph.ppmi.p2m, com.p2pqrpay, thailand.pwc, qris, georgia.customer.k, PIX, QR3. |
additionalData | object | Opaque. Its shape depends on the QR flavour. Pass it through verbatim rather than parsing it. |
The public endpoints return no response signature.
additionalData and recipientName are what create-sell-order wants in extendInfo. Keep them as
they arrive rather than rebuilding them.
Errors — 33 QR_CODE_NOT_SUPPORTED (unreadable or unsupported format), 1. None of the shared
signature errors apply.
Generate a test QR code
POST /api/v2/public/generate-philippines-qrcode
A sandbox helper that mints a Philippine QR code, so you have something to decode. It is public, but unlike the endpoint above, it validates what you send.
Request — amount (number, minimum 200). Response data — qrContent, the raw string you
feed back into get-qr-code-info.
Prices and limits
POST /api/v2/orders/prices
Current crypto prices, and the sell limits. The limits vary by currency and by payment provider, so read them here rather than hard-coding them.
Signed string
partnerCode|currencies|network|<secretKey>
fiatCurrency and service are not signed.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
partnerCode | string | Yes | |
currencies | string | Yes | Comma-separated: USDT,ETH,BTC. Signed verbatim, so the order matters. |
network | string | Yes | ERC20, TRC20, TON… |
fiatCurrency | string | No | Defaults to VND. |
service | string | No | Defaults to SELL, which is the value scan-to-pay quotes under. Leave it. |
signature | string | Yes |
Response data
| Field | Type | Notes |
|---|---|---|
network | string | Echoed back. |
currencies | array | Per token: name, price, minSell, maxSell, minBuy, maxBuy. The four bounds are optional and may be absent. |
signature | string | Over network|secretKey. The token list itself is not signed. |
Prices are cached for up to 10 minutes, so quote with review-sell-order before
you commit to an amount.
Errors — 6 TOKEN_NOT_SUPPORTED_ON_NETWORK, 14 UNKNOWN_SERVICE_TYPE, 46
SERVICE_NOT_AVAILABLE, 1.
List supported banks
POST /api/v2/bank/get-supported-banks
Signed string
partnerCode|<secretKey>
Request — partnerCode (required), fiatCurrency (optional, defaults to VND), signature.
fiatCurrency is not part of the signed string.
Response data — banks, an array of bankCode / bankName / logo, and a signature over
secretKey alone. No other field goes into it, so the bank list is not covered by the signature.
A temporary interruption at the banking partner can answer success: true with an empty banks
array. Treat an empty list as a fetch to retry later rather than as the currency having no banks.
Errors — 1, plus the shared ones.
Verify a bank account
POST /api/v2/bank/account/verify
Resolves an account number to the name it is held under, so you can show the user who they are about to pay.
Signed string
partnerCode|bankCode|bankAccountNumber|<secretKey>
fiatCurrency is not signed.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
partnerCode | string | Yes | |
bankCode | string | Yes | |
bankAccountNumber | string | Yes | |
fiatCurrency | string | No | Defaults to VND. Only VND, NGN, GHS, ZAR, KES are supported — anything else returns 53. |
signature | string | Yes |
Response data — bankCode, bankAccountNumber, bankAccountName, and a signature over
bankCode\|bankAccountNumber\|bankAccountName\|secretKey.
Errors — 18 INVALID_BANK_ACCOUNT, 53 FIAT_CURRENCY_NOT_SUPPORTED, 1.
For VND, verification failures are reported as 1, an unsupported bank among them. When a VND
check answers 1, recheck the bankCode and the account number first.
Quote an order
POST /api/v2/orders/review-sell-order
What the order will cost in crypto, and the fees, before you commit. Nothing is created, and nothing is debited.
Signed string
partnerCode|currency|fiatAmount|<secretKey>
If you omit fiatAmount, the literal 0 takes its place in the string — sign …|0|, not …||.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
partnerCode | string | Yes | |
currency | string | Yes | The crypto you are selling — USDT. |
fiatAmount | number | No | The fiat you want the merchant to receive. |
tokenAmount | number | No | Quote from the crypto side instead. Not signed. |
fiatCurrency | string | No | Defaults to VND. Not signed. |
userEmail | string | No | Not signed, unlike create-sell-order, where it is. |
signature | string | Yes |
Response data
| Field | Type | Notes |
|---|---|---|
fiatAmount | number | Echoed from your request — if you quoted by tokenAmount, this is undefined, and the response signature is computed over that literal text. |
currency | string | The crypto code. |
price | number | |
tokenAmount | number | The crypto that will be debited. |
totalPayment | number | |
processingFee, systemFee | number | |
signature | string | Over fiatAmount|currency|price|tokenAmount|totalPayment|secretKey. |
Create an order
POST /api/v2/orders/create-sell-order
Creates the order, debits your fund, and pays the merchant.
Signed string
partnerCode|externalOrderId|currency|fiatAmount|bankCode|bankAccountNumber|content|userEmail|<secretKey>
An omitted fiatAmount becomes the literal 0 in that string. bankAccountNumber is in it too, and
leaving that out is not rejected as a missing field. You simply end up signing the literal text
undefined.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
partnerCode | string | Yes | |
externalOrderId | string | Yes | Your unique id. Also the idempotency key — a repeat returns 11. |
currency | string | Yes | The crypto to debit — USDT. |
userEmail | string | Yes | The end user. Must be KYC-verified. |
bankCode | string | Yes | From the QR decoding, or the bank list. |
content | string | Yes | Transfer remark. Unaccented letters, no special characters. |
bankAccountNumber | string | Yes | Omitting it is not rejected as missing, but it is signed — see above. |
fiatAmount | number | No | The fiat the merchant receives. |
fiatCurrency | string | No | Defaults to VND. |
webhookSecretKey | string | No | Without it the order settles silently, read through orders/details. See Webhooks. |
extendInfo | object | per currency | The decoded QR data — see below. |
signature | string | Yes |
extendInfo
This is how the decoded QR data reaches the payment provider. It holds a qrType and one country key.
The country key carries recipientName and the opaque additionalData, both taken verbatim from
get-qr-code-info.
fiatCurrency | Key | Required? |
|---|---|---|
PHP | PH | recipientName is required, and is rejected when it is all digits and the account number is all digits too. |
GEL | GE | Required. Omit it and the request fails with 1 — see below. |
BRL | BR | Required in practice. |
ARS | AR | Required in practice. |
PEN | PE | Required in practice. |
VND, NGN, others | — | Send extendInfo: null. |
{
"partnerCode": "ACME",
"externalOrderId": "acme-1042",
"currency": "USDT",
"fiatCurrency": "PHP",
"fiatAmount": 500,
"bankCode": "UBPHPHMM",
"bankAccountNumber": "109645950075",
"content": "coffee",
"webhookSecretKey": "81663eae...",
"extendInfo": {
"qrType": "com.p2pqrpay",
"PH": {
"recipientName": "VIETNAMIT BISTRO",
"additionalData": { "tfrAcctNo": "109645950075", "uniqueId": "ph.ppmi.p2m" }
}
},
"signature": "I7XDfTngWIraq...=="
}
For those three currencies the country keys are read in the fixed order BR → AR → PE, and the
first one present wins, whichever currency you sent. An ARS order carrying a stray BR block pays
out against the BR data, so send exactly the one key that matches your fiatCurrency.
For GEL, omitting extendInfo.GE fails with 1 INTERNAL_SERVER_ERROR.
The ID, TH and VN keys are accepted but never read.
Response data — the order object, with status: "AWAITING_PAYMENT".
Errors — beyond the shared ones:
11 DUPLICATE_TRANSACTION_ID | externalOrderId is already used. Fetch the order instead. |
13 WEBHOOK_SECRET_NOT_FOUND | webhookSecretKey matches no registered endpoint. The order is not created. |
7 PARTNER_INSUFFICIENT_BALANCE | Your fund cannot cover it. |
24 / 25 | Below the minimum / above the maximum. |
36 / 37 / 42 | Per-transaction, daily, monthly cap. |
34, 59, 60, 61, 62, 64 | The user's KYC or nationality blocks the order. |
17 INVALID_TRANSFER_CONTENT | content has accents or special characters. |
18 INVALID_BANK_ACCOUNT | The account could not be verified, or recipientName is missing or invalid. |
44 INVALID_PAYMENT_INFO | A PHP QR order is missing its additionalData. |
56 BANK_NOT_SUPPORTED | bankCode is not in the provider's list. |
65 HIGH_RISK_BANK_ACCOUNT | The recipient is blacklisted. |
46 SERVICE_NOT_AVAILABLE | Scan-to-pay is off for you, or no provider covers this currency. |
12 TRANSACTION_CREATE_FAILED | The payout failed after the order was created. You are already refunded, and data holds the order. |
Get an order
POST /api/v2/orders/details
The source of truth. The webhook is a convenience; this is what you reconcile against.
Signed string
partnerCode|externalOrderId|<secretKey>
Request — partnerCode, externalOrderId, signature. All required.
Response data — the order object.
Errors — 15 TRANSACTION_NOT_FOUND (wrong id, or the order belongs to another partner), plus
the shared ones.