Quote for checkout confirmation: creates an upstream pending-payment order without holding funds
x-on-behalf-of required
funding_source defaults to member; merchant is available only in merchant-hosted mode and debits the merchant prepaid account. Merchant debit = the current upstream digital-asset quote principal + platform fee. The receiving merchant receives the full acquirer_amount. The platform fee is principal × the merchant fee rate. An unset or 0% rate charges nothing; a nonzero rate has a minimum fee of 0.01 U. Fees default to USDT. If the balance after deducting any same-asset principal cannot cover the complete fee, the entire fee uses available USDC instead; fees are never split. Principal remains in the selected quote asset; changing the fee asset never converts the principal. Merchant-funded per-transaction and daily payment limits count principal only, excluding fees; balances must still cover principal plus fees.
Merchant-funded responses additionally include fee_asset, fee_amount as a decimal string, and fee_ledger_scale. customer_total and the legacy fee describe only the debit and same-asset fee in asset. A same-asset fee is already included in customer_total and must not be added again. For a different fee asset, fee=0, and fee_amount of fee_asset is additionally debited. The confirmation screen must show both assets separately, never combine them into one number. For example, principal 10 USDT and fee 0.1 USDC give customer_total="10.000000", fee="0.000000", fee_asset="USDC", and fee_amount="0.100000". Insufficient balance in either asset prevents payment.
Decoding only tells you to pay 250 THB; this endpoint tells you how much digital asset the member will be debited. Without it, you cannot build a confirmation screen and would have to call POST /v1/qrpay/payments directly, showing users the amount for the first time after funds were locked.
⚠⚠ This step already creates a pending-payment transaction upstream; it is not a free read-only estimate like POST /v1/remit/quotes. Therefore:
- The same member +
code_value+currency+amountreuses the same upstream order for 60 seconds, without another outbound call. - 30 quotes per member / 60 seconds, in addition to merchant-level limits; both must pass.
- Uses
qrpay:write, not:read, because it has upstream side effects.
Polling quotes creates upstream pending orders that will never be paid and rate-limits your own member with 429. Call once per checkout.
⚠ customer_total is indicative, not a promise. POST /v1/qrpay/payments obtains a fresh quote itself before holding funds, because upstream quotes expire while users remain on confirmation screens. Holding against an old quote would settle upstream at a new price while charging the old price locally. The payments response is authoritative; the amounts may differ by a few minor units.
⚠ expires_at is supplied by the upstream; an empty string means none was supplied. A quote reused from the 60-second memo may be near or past expiry. Inspect this field; do not assume a newly retrieved response contains a fresh quote.
⚠ You do not determine the debit asset. chosen follows the member’s own payment priorities. options contains all assets quoted by the upstream that we also enable. options[].asset is exactly the valid set for payments’ preferred_asset. An asset outside that set is rejected immediately, never silently replaced.
sufficient indicates whether balances cover this payment plan; merchant mode checks both principal and fee assets. Balance figures are not returned. A nonempty blocked_reason makes this quote unpayable.
⚠ Codes with nonempty required_payer_fields cannot proceed through the Open API; see POST /v1/qrpay/decode. This endpoint also does not accept payer fields.
Prerequisites
- QR payments enabled for you and at least one usable upstream available
- This QR standard is supported; see
GET /v1/qrpay/schemes
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
x-on-behalf-of |
string | Required | Member receiving the quote. Required: the debit depends on their payment preferences and balance; the upstream KYC flag also uses their actual level. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
funding_source |
"member" | "merchant" | Optional | Pay from the member wallet or merchant prepaid account; merchant-funded payments do not debit the member wallet. |
code_value |
string | Required | Original QR payload, which must match the value later sent to payments. |
currency |
string | Optional | Acquiring-side fiat currency, such as THB. Supply only for custom-amount codes, amount_editable: true; prohibited for fixed-amount codes. |
amount |
string | Optional | Acquiring-side fiat amount, such as "250.00", not a digital-asset quantity. Supply only for custom-amount codes. |
preferred_asset |
string | Optional | Selected principal asset, such as USDT; merchant-funded payment prefers USDT by default. Explicit selection never silently switches assets. |
payer |
object | Optional | Additional payer information. Required only when required_payer_fields from POST /v1/qrpay/decode is nonempty, using exactly those keys: custName / custFirstName / custLastName / custNationCode / gender / mobile / email / legalNationCode / legalType / legalId. Unknown keys are discarded, never forwarded upstream. |
Response
customer_total / fee are fixed-point decimal strings, with precision from the same item’s ledger_scale.
Use that for parsing, not display_scale, which only controls checkout display precision and would lose information if used for parsing.
acquirer_currency / acquirer_amount describe acquiring-side fiat.
They are neither the same number nor the same currency as customer_total:
member payment includes upstream quote + our cost protection + premium + fee and is inherently larger.
A nonempty blocked_reason means payments will reject immediately with that code, such as a per-transaction or daily limit,
below-minimum amount, or KYC trigger. It applies only to chosen;
choosing another asset from options requires evaluating that option again.
An empty reason does not guarantee execution: payments requotes and reruns all gates.{
"payee": "Bangkok Coffee Co.",
"acquirer_currency": "THB",
"acquirer_amount": "250.00",
"expires_at": "2026-08-13T09:31:05Z",
"chosen": {
"asset": "USDT",
"ledger_scale": 6,
"display_scale": 2,
"customer_total": "7.320000",
"fee": "0.030000",
"sufficient": true
},
"options": [
{
"asset": "USDT",
"ledger_scale": 6,
"display_scale": 2,
"customer_total": "7.320000",
"fee": "0.030000",
"sufficient": true
},
{
"asset": "USDC",
"ledger_scale": 6,
"display_scale": 2,
"customer_total": "7.321000",
"fee": "0.030000",
"sufficient": false
}
],
"blocked_reason": ""
}qr_code_invalid: undecodable code or upstream rejection. · corridor_not_supported: corridor unavailable; ten other codes would not help, so do not ask users to rescan. · amount_out_of_range: amount outside this code’s allowed range. · kyc_required: KYC threshold triggered. · insufficient_balance: no usable debit asset, because none of the upstream’s assets is enabled here. · request_rejected: member blocklisted, account frozen, or card funds protected. · product_not_available: line not enabled for you. · service_unavailable: no usable upstream now. · invalid_request · member_context_required · member_not_foundinsufficient_scope: this key lacks qrpay:writerate_limited: member’s quote allowance in 60 seconds exhausted, or your overall write quota exhausted. Back off according to Retry-After; do not retry immediately.upstream_error: upstream 5xx, network failure, or rejection without a more specific reason. No funds have moved; only pending-order creation failed, so retrying unchanged is safe.curl -X POST 'https://api.zinfra.vip/v1/qrpay/quotes' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-on-behalf-of: $MEMBER_ID' \
-H 'content-type: application/json' \
-d '{
"code_value": "00020101021229300012D156000000000510A93FO3230Q...",
"currency": "THB",
"amount": "250.00",
"payer": {
"custName": "CHAN TAI MAN",
"legalId": "A1234567"
}
}'const res = await fetch("https://api.zinfra.vip/v1/qrpay/quotes", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"content-type": "application/json",
},
body: JSON.stringify({
"code_value": "00020101021229300012D156000000000510A93FO3230Q...",
"currency": "THB",
"amount": "250.00",
"payer": {
"custName": "CHAN TAI MAN",
"legalId": "A1234567"
}
}),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();import requests
res = requests.post(
"https://api.zinfra.vip/v1/qrpay/quotes",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"content-type": "application/json",
},
json={
"code_value": "00020101021229300012D156000000000510A93FO3230Q...",
"currency": "THB",
"amount": "250.00",
"payer": {
"custName": "CHAN TAI MAN",
"legalId": "A1234567"
}
},
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/qrpay/quotes",
strings.NewReader(`{
"code_value": "00020101021229300012D156000000000510A93FO3230Q...",
"currency": "THB",
"amount": "250.00",
"payer": {
"custName": "CHAN TAI MAN",
"legalId": "A1234567"
}
}`))
req.Header.Set("x-auth-token", "Bearer $TOKEN")
req.Header.Set("x-on-behalf-of", "$MEMBER_ID")
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
// Decode amount fields as string, not float64.HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.zinfra.vip/v1/qrpay/quotes"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-on-behalf-of", "$MEMBER_ID")
.header("content-type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"code_value": "00020101021229300012D156000000000510A93FO3230Q...",
"currency": "THB",
"amount": "250.00",
"payer": {
"custName": "CHAN TAI MAN",
"legalId": "A1234567"
}
}
"""))
.build();
// Use String / BigDecimal for amounts, not double.$ch = curl_init('https://api.zinfra.vip/v1/qrpay/quotes');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
'x-on-behalf-of: $MEMBER_ID',
'content-type: application/json',
],
CURLOPT_POSTFIELDS => <<<'JSON'
{
"code_value": "00020101021229300012D156000000000510A93FO3230Q...",
"currency": "THB",
"amount": "250.00",
"payer": {
"custName": "CHAN TAI MAN",
"legalId": "A1234567"
}
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
{
"payee": "Bangkok Coffee Co.",
"acquirer_currency": "THB",
"acquirer_amount": "250.00",
"expires_at": "2026-08-13T09:31:05Z",
"chosen": {
"asset": "USDT",
"ledger_scale": 6,
"display_scale": 2,
"customer_total": "7.320000",
"fee": "0.030000",
"sufficient": true
},
"options": [
{
"asset": "USDT",
"ledger_scale": 6,
"display_scale": 2,
"customer_total": "7.320000",
"fee": "0.030000",
"sufficient": true
},
{
"asset": "USDC",
"ledger_scale": 6,
"display_scale": 2,
"customer_total": "7.321000",
"fee": "0.030000",
"sufficient": false
}
],
"blocked_reason": ""
}