Exchange quote: read-only by default; supply lock to lock the price
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 noquote_id. Use it to display a number; execution recalculates at the then-current price. lock: true: lock the price. Returnsquote_idandquote_expires_at. Supply it toPOST /v1/exchange/ordersbefore 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→USDandUSD→USDTare configured independently - Member is neither frozen nor closed and meets the direction’s minimum KYC level
Request Headers
| Field | Type | Required | Description |
|---|---|---|---|
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
| Field | Type | Required | Description |
|---|---|---|---|
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
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
}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.insufficient_scope: this key lacks exchange:writecurl -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.
{
"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
}