Z Zise Developers 简体中文

Exchange quote: read-only by default; supply lock to lock the price

POST /v1/exchange/quotes scope: exchange:write
On behalf of a member · x-on-behalf-of required

Choose one of two modes in the request body:

  • Omit lock, the default: read-only, no order or fund hold, and no quote_id. Use it to display a number; execution recalculates at the then-current price.
  • lock: true: lock the price. Returns quote_id and quote_expires_at. Supply it to POST /v1/exchange/orders before expiry to execute at that quote. Expired quotes are always rejected; we never execute at a new price on your behalf, which would impose pricing you did not accept.

⚠ quote_id is the order ID itself, the same exc_<uuid>. Locking creates a real order row with status quoted, visible in GET /v1/exchange/orders with status quoted and retrievable through GET /v1/exchange/orders/{id}. If execution times out, query this ID before blindly retrying to determine whether execution occurred. Unused locked quotes expire without any movement of funds.

⚠ The lock covers price, not authorization. Execution still checks account restrictions, disabled directions, strong-authentication requirements, daily limits, member balances, and your prepaid funds. The lock only guarantees which numbers are used.

⚠ Locking is not the default; request it when needed. Frequent display-only calls would otherwise fill reconciliation lists with unexecuted quoted rows. A locked quote is also a one-sided benefit to you during its window, so we issue it on request.

⚠ lock must be a JSON boolean. "true" / 1 return invalid_fields, never silently treated as false.

⚠ The amount parameter is from_amount, not amount, as on execution. The wrong field causes amount parsing to fail with invalid_request.

⚠ The idempotency key is optional here. Without locking it does nothing; with locking, supplying it lets a timeout retry recover the same locked quote, rather than creating two quotes of which only one is used.

Prerequisites

  • Direction enabled; USDT→USD and USD→USDT are configured independently
  • Member is neither frozen nor closed and meets the direction’s minimum KYC level
FieldTypeRequiredDescription
x-on-behalf-of string Required Member for whom the quote is requested. Fees and limits use that member’s merchant configuration.
x-idempotency-key string Optional Optional; meaningful only with lock: true. The same key, direction, and amount retrieve the same locked quote on retry instead of creating another.

Request Body

FieldTypeRequiredDescription
from_asset string Required Source asset code, such as USDT. Case-insensitive; uppercased by the server.
to_asset string Required Destination asset code, such as USD. Must differ from from_asset.
from_amount string Required Source asset amount to debit. Fixed-point string using from_asset’s ledger_scale. Extra decimals with nonzero trailing digits are rejected, never rounded.USDT uses 6 decimals, for example 500.000000
lock boolean Optional Whether to lock the price. Defaults to false, a read-only quote whose response omits quote_id entirely rather than returning null. true returns quote_id + quote_expires_at; execution before expiration uses this quote. ⚠ Must be a JSON boolean. "true" / 1 return invalid_fields.

Response

200Quote obtained. fee_asset may be on the from or to side, depending on direction configuration. It therefore includes its own asset code; do not assume either side. quote_id and quote_expires_at appear only with lock: true. Both modes return quote_ttl_sec, but with different meanings: a real lock window when locked, or merely an indication of how long the displayed number may remain relevant otherwise.
{
  "from_asset": "USDT",
  "to_asset": "USD",
  "from_amount": "500.000000",
  "to_amount": "498.750000",
  "rate": "1.0000",
  "fee_amount": "1.250000",
  "fee_asset": "USD",
  "quote_id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45",
  "quote_expires_at": "2026-08-13T09:31:30Z",
  "quote_ttl_sec": 90,
  "ledger_scale_from": 6,
  "ledger_scale_to": 6
}
400Registered public codes: invalid_request, invalid body / amount format / missing asset code; invalid_fields, nonboolean lock; request_rejected, member frozen, closed, or rejected by risk control; service_unavailable, line currently unavailable; member_context_required; member_not_found. ⚠ Business rejections still mapped to 500 api_error, as described in the introduction: direction disabled / asset delisted; identical from and to; below minimum exchange amount; above per-transaction maximum; daily amount or count exceeded; insufficient KYC; rate temporarily unavailable; fees consume principal. Do not retry 500 indefinitely.
403insufficient_scope: this key lacks exchange:write
Request
curl -X POST 'https://api.zinfra.vip/v1/exchange/quotes' \
  -H 'x-auth-token: Bearer $TOKEN' \
  -H 'x-on-behalf-of: $MEMBER_ID' \
  -H 'content-type: application/json' \
  -d '{
    "from_asset": "USDT",
    "to_asset": "USD",
    "from_amount": "500.000000",
    "lock": true
  }'
const res = await fetch("https://api.zinfra.vip/v1/exchange/quotes", {
  method: "POST",
  headers: {
    "x-auth-token": "Bearer $TOKEN",
    "x-on-behalf-of": "$MEMBER_ID",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    "from_asset": "USDT",
    "to_asset": "USD",
    "from_amount": "500.000000",
    "lock": true
  }),
});
// Keep monetary amounts as strings, never numbers.
const data = await res.json();
import requests

res = requests.post(
    "https://api.zinfra.vip/v1/exchange/quotes",
    headers={
        "x-auth-token": "Bearer $TOKEN",
        "x-on-behalf-of": "$MEMBER_ID",
        "content-type": "application/json",
    },
    json={
      "from_asset": "USDT",
      "to_asset": "USD",
      "from_amount": "500.000000",
      "lock": true
    },
)
# Use Decimal(str(...)) for amounts, not float.
data = res.json()
req, _ := http.NewRequest("POST", "https://api.zinfra.vip/v1/exchange/quotes",
    strings.NewReader(`{
  "from_asset": "USDT",
  "to_asset": "USD",
  "from_amount": "500.000000",
  "lock": true
}`))
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/exchange/quotes"))
    .header("x-auth-token", "Bearer $TOKEN")
    .header("x-on-behalf-of", "$MEMBER_ID")
    .header("content-type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
{
  "from_asset": "USDT",
  "to_asset": "USD",
  "from_amount": "500.000000",
  "lock": true
}
"""))
    .build();
// Use String / BigDecimal for amounts, not double.
$ch = curl_init('https://api.zinfra.vip/v1/exchange/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'
{
  "from_asset": "USDT",
  "to_asset": "USD",
  "from_amount": "500.000000",
  "lock": true
}
JSON,
]);
$res = curl_exec($ch);
// Use bcmath / strings for amounts, not floatval.
200
{
  "from_asset": "USDT",
  "to_asset": "USD",
  "from_amount": "500.000000",
  "to_amount": "498.750000",
  "rate": "1.0000",
  "fee_amount": "1.250000",
  "fee_asset": "USD",
  "quote_id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45",
  "quote_expires_at": "2026-08-13T09:31:30Z",
  "quote_ttl_sec": 90,
  "ledger_scale_from": 6,
  "ledger_scale_to": 6
}