Skip to main content

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:

  1. Decode the QR — get-qr-code-info. Public, no signature.
  2. Quote it — review-sell-order.
  3. Create the order — create-sell-order, passing the decoded QR data through in extendInfo.
  4. 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.

FieldTypeNotes
externalOrderIdstringYour id for the order.
typestringSELL for scan-to-pay, not SCAN_TO_PAY. CASHOUT for a cashout.
fiatAmountnumberThe fee-inclusive total you are charged, not the amount the merchant receives.
paidAmountnumber
statusstringSee below.
descriptionsstringPlural. Success, or the failure reason.
createdAtstring2026-07-14 10:32:07 — UTC+7, no zone marker.
expiresAtstringCreated + 15 minutes, same format.
tokenTransferobjectcurrency, network, price, amount, address, txHash, memo.
bankTransferobjectbankAccountName, bankAccountNumber, bankName, bankCode, contentPayment, totalPayment, qrUrl.
feesobjectsystemFee, processingFee.
signaturestringOver externalOrderId|type|fiatAmount|status|secretKey.

Four fields the older docs describe differently:

  • The wallet field is address, not walletAddress, and there is a memo beside it.
  • The description field is descriptions, plural.
  • bankTransfer carries a bankCode that the old schema does not list.
  • bankTransfer.expiresAt is on the wire but always empty. Use the order-level expiresAt.

Status

StatusMeaning
AWAITING_PAYMENTCreated; the payout has not gone out yet.
PAYMENT_COMPLETEDThe recipient has been paid.
PROCESSINGIn flight.
SUCCESSDone. Terminal.
ERRORFailed. 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

FieldTypeNotes
bankCode, bankNamestringFeed bankCode straight into create-sell-order.
bankAccountNumberstringLikewise.
recipientNamestring
countryCodestringVN, PH, TH, GE, BR, AR, PE.
amountnumberPresent only if the QR fixes an amount.
contentstring
qrTypestringvietqr, ph.ppmi.p2m, com.p2pqrpay, thailand.pwc, qris, georgia.customer.k, PIX, QR3.
additionalDataobjectOpaque. 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.

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

Requestamount (number, minimum 200). Response dataqrContent, 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

FieldTypeRequiredNotes
partnerCodestringYes
currenciesstringYesComma-separated: USDT,ETH,BTC. Signed verbatim, so the order matters.
networkstringYesERC20, TRC20, TON
fiatCurrencystringNoDefaults to VND.
servicestringNoDefaults to SELL, which is the value scan-to-pay quotes under. Leave it.
signaturestringYes

Response data

FieldTypeNotes
networkstringEchoed back.
currenciesarrayPer token: name, price, minSell, maxSell, minBuy, maxBuy. The four bounds are optional and may be absent.
signaturestringOver 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.

Errors6 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>

RequestpartnerCode (required), fiatCurrency (optional, defaults to VND), signature. fiatCurrency is not part of the signed string.

Response databanks, 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.

An empty list is not an error

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

Errors1, 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

FieldTypeRequiredNotes
partnerCodestringYes
bankCodestringYes
bankAccountNumberstringYes
fiatCurrencystringNoDefaults to VND. Only VND, NGN, GHS, ZAR, KES are supported — anything else returns 53.
signaturestringYes

Response databankCode, bankAccountNumber, bankAccountName, and a signature over bankCode\|bankAccountNumber\|bankAccountName\|secretKey.

Errors18 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

FieldTypeRequiredNotes
partnerCodestringYes
currencystringYesThe crypto you are selling — USDT.
fiatAmountnumberNoThe fiat you want the merchant to receive.
tokenAmountnumberNoQuote from the crypto side instead. Not signed.
fiatCurrencystringNoDefaults to VND. Not signed.
userEmailstringNoNot signed, unlike create-sell-order, where it is.
signaturestringYes

Response data

FieldTypeNotes
fiatAmountnumberEchoed from your request — if you quoted by tokenAmount, this is undefined, and the response signature is computed over that literal text.
currencystringThe crypto code. Some schemas type this as a number; it is a string.
pricenumber
tokenAmountnumberThe crypto that will be debited.
totalPaymentnumber
processingFee, systemFeenumber
signaturestringOver 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

FieldTypeRequiredNotes
partnerCodestringYes
externalOrderIdstringYesYour unique id. Also the idempotency key — a repeat returns 11.
currencystringYesThe crypto to debit — USDT.
userEmailstringYesThe end user. Must be KYC-verified.
bankCodestringYesFrom the QR decoding, or the bank list.
contentstringYesTransfer remark. Unaccented letters, no special characters.
bankAccountNumberstringYesOmitting it is not rejected as missing, but it is signed — see above.
fiatAmountnumberNoThe fiat the merchant receives.
fiatCurrencystringNoDefaults to VND.
webhookSecretKeystringNoOmit it and no callback is ever sent. See Webhooks.
extendInfoobjectper currencyThe decoded QR data — see below.
signaturestringYes

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.

fiatCurrencyKeyRequired?
PHPPHrecipientName is required — without it the order fails with 44 INVALID_PAYMENT_INFO.
GELGERequired. Omit it and the request fails with 1 — see below.
BRLBRRequired in practice.
ARSARRequired in practice.
PENPERequired in practice.
VND, NGN, othersSend extendInfo: null.
JSON
{
"partnerCode": "ACME",
"externalOrderId": "acme-1042",
"currency": "USDT",
"fiatCurrency": "PHP",
"fiatAmount": 500,
"bankCode": "UBPHPHMM",
"bankAccountNumber": "109645950075",
"content": "coffee",
"userEmail": "[email protected]",
"webhookSecretKey": "81663eae...",
"extendInfo": {
"qrType": "com.p2pqrpay",
"PH": {
"recipientName": "VIETNAMIT BISTRO",
"additionalData": { "tfrAcctNo": "109645950075", "uniqueId": "ph.ppmi.p2m" }
}
},
"signature": "I7XDfTngWIraq...=="
}
BRL, ARS and PEN share one lookup

For those three currencies the country keys are read in the fixed order BRARPE, 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 datathe order object, with status: "AWAITING_PAYMENT".

Errors — beyond the shared ones:

11 DUPLICATE_TRANSACTION_IDexternalOrderId is already used. Fetch the order instead.
13 WEBHOOK_SECRET_NOT_FOUNDwebhookSecretKey matches no registered endpoint. The order is not created.
7 PARTNER_INSUFFICIENT_BALANCEYour fund cannot cover it.
24 / 25Below the minimum / above the maximum.
36 / 37 / 42Per-transaction, daily, monthly cap.
34, 59, 60, 61, 62, 64The user's KYC or nationality blocks the order.
17 INVALID_TRANSFER_CONTENTcontent has accents or special characters.
18 INVALID_BANK_ACCOUNTThe account could not be verified.
44 INVALID_PAYMENT_INFOA PHP order is missing recipientName.
56 BANK_NOT_SUPPORTEDbankCode is not in the provider's list.
65 HIGH_RISK_BANK_ACCOUNTThe recipient is blacklisted.
46 SERVICE_NOT_AVAILABLEScan-to-pay is off for you, or no provider covers this currency.
12 TRANSACTION_CREATE_FAILEDThe 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>

RequestpartnerCode, externalOrderId, signature. All required.

Response datathe order object.

Errors15 TRANSACTION_NOT_FOUND (wrong id, or the order belongs to another partner), plus the shared ones.