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. We then debit 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.

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.

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.

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.

Errors — 33 QR_CODE_NOT_SUPPORTED (unreadable or unsupported format), 20 SERVICE_TEMPORARILY_UNAVAILABLE (the service is temporarily unavailable; retry later), 1. None of the shared signature errors apply.

Generate a test QR code​

POST /api/v2/public/generate-philippines-qrcode

A sandbox helper that generates a Philippine QR code, so you have something to decode. It is public and needs no signature.

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

FieldTypeRequiredNotes
partnerCodestringYes
currenciesstringYesComma-separated: USDT,ETH,BTC. Signed verbatim, so the order matters.
networkstringYesERC20, TRC20, TON…
fiatCurrencystringNoDefaults to VND.
servicestringNoDefaults to SELL, the value for scan-to-pay, so you can omit 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.

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, 53 FIAT_CURRENCY_NOT_SUPPORTED (the message names the currency), 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.

Treat an empty bank list as a fetch to retry

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

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

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

Errors — 18 INVALID_BANK_ACCOUNT, 53 FIAT_CURRENCY_NOT_SUPPORTED, 56 BANK_NOT_SUPPORTED (the bankCode is not in the list from get-supported-banks), 1.

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. No decimals for VND, up to two for PHP, ARS, BRL and PEN; see Amount precision.
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.
pricenumber
tokenAmountnumberThe crypto that will be debited.
totalPaymentnumber
processingFee, systemFeenumber
signaturestringOver fiatAmount|currency|price|tokenAmount|totalPayment|secretKey.

Amount precision​

Maximum decimal places in fiatAmount, by currency:

CurrencyMaximum decimal placesExample
VND0100000
IDR0150000
PHP2500.25
ARS21334.85
BRL2100.99
PEN250.55

Extra digits are cut off, not rounded, and the request goes through: PHP 12.345 is priced and paid as 12.34. review-sell-order still echoes the fiatAmount you sent (12.345), while its tokenAmount is priced on 12.34. Send no more decimal places than the currency allows, so that the quoted fiatAmount is the amount paid. Cashout uses the same table.

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: if you leave it out, the request is not rejected as missing a field, and its segment is signed as 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. No decimals for VND, up to two for PHP, ARS, BRL and PEN; see Amount precision.
fiatCurrencystringNoDefaults to VND.
webhookSecretKeystringNoWithout it the order settles silently, read through orders/details. See Webhooks.
extendInfoobjectper currencyThe decoded QR data — see below.
signaturestringYes

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, and is rejected when it is all digits and the account number is all digits too.
GELGERequired. Omit it and the request fails with 1 — see below.
BRLBRRequired in practice.
ARSARRequired in practice.
PENPERequired in practice.
VND, NGN, others—Send 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 — send exactly one country key

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_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.
142 KYC_EXTRA_INFO_REQUIRED_FOR_LATAM_LOCAL_USERThe user's KYC profile lacks the address or gender this market requires; the message names them. See Markets that need extra profile fields.
17 INVALID_TRANSFER_CONTENTcontent has accents or special characters.
18 INVALID_BANK_ACCOUNTThe account could not be verified, or recipientName is missing or invalid.
44 INVALID_PAYMENT_INFOA PHP QR order is missing its additionalData.
56 BANK_NOT_SUPPORTEDbankCode is not in the provider's list.
65 HIGH_RISK_BANK_ACCOUNTThe recipient is flagged as high risk.
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

Returns the current state of an order. Use it to confirm a webhook and to reconcile your records.

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.