Z Zise Developers 简体中文

Quote for checkout confirmation: creates an upstream pending-payment order without holding funds

POST /v1/qrpay/quotes scope: qrpay:write
On behalf of a member · 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 + amount reuses 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
FieldTypeRequiredDescription
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

FieldTypeRequiredDescription
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

200customer_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": ""
}
400qr_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_found
403insufficient_scope: this key lacks qrpay:write
429rate_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.
502upstream_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.
Request
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.
200
{
  "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": ""
}