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. |
Four fields the older docs describe differently:
- The wallet field is
address, notwalletAddress, and there is amemobeside it. - The description field is
descriptions, plural. bankTransfercarries abankCodethat the old schema does not list.bankTransfer.expiresAtis on the wire but always empty. Use the order-levelexpiresAt.
Status
| 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. |
ERROR is the failure status. Older documentation also lists FAIL, FAILED and
PROCESSING_TOKEN_TRANSFER. No v2 order carries any of them, and FAILED belongs to v3. 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. |
minFiatPayment appears in some older schemas. It is never returned.
Responses are cached for 10 minutes, keyed on every request field, so a price you read here can be ten
minutes old. 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.
If the upstream provider fails, the error is swallowed and you get success: true with an empty
banks array. Treat an empty list as a failure to fetch, not as "this currency has 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.
On the VND path every failure collapses to 1, including "bank not supported", which never reaches
you as 56. A 1 here is more often a bad bankCode than a server fault.
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. Some schemas type this as a number; it is a string. |
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 | Omit it and no callback is ever sent. See Webhooks. |
extendInfo | object | per currency | The decoded QR data — see below. |
signature | string | Yes |
userKYCVerified appears in older docs but no longer exists; send it and it is ignored. KYC is read
from our own record of the user, not from a flag you assert.
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 — without it the order fails with 44 INVALID_PAYMENT_INFO. |
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. Send exactly one country key.
GEL behaves worse still. If extendInfo.GE is missing, the server throws, and you get 1
INTERNAL_SERVER_ERROR instead of an error that tells you what is wrong.
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. |
44 INVALID_PAYMENT_INFO | A PHP order is missing recipientName. |
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.