兑换成交(原子 · 不可逆 · 无中间态)
x-on-behalf-of
需 x-idempotency-key
动钱 · 扣该会员的 from 资产 + 增他的 to 资产(同一名下,原子)
失败处置见下方响应表。超时(504)用同一把幂等键重试——我方可能已经处理完;业务失败要换新键,同键会原样返回那次失败。
建单与成交合成一步。 会员端分两步是因为客户端要给用户一屏确认;
你的服务器不需要那一屏,而两步意味着你要自己管一张很快过期的报价单
—— 那是一类纯粹由接口形态造出来的失败。
幂等:同一把 x-idempotency-key 重放拿回同一张单(响应 200 +
duplicated: true),不会成交第二次。
两种定价形态,由你带不带 quote_id 决定:
- 不带 —— 成交那一刻重新报一次价,按当时的价成交。你拿到的价
可能与几秒前 POST /v1/exchange/quotes 看到的不同。
- 带 —— 按
POST /v1/exchange/quotes+lock: true拿到的那份
报价成交。要给终端用户一屏确认的,走这一条。
⚠ 带 quote_id 时 from_asset / to_asset / from_amount
变成可选(只传 quote_id 即可)。传了就必须与那份报价一致,
对不上一律 invalid_fields —— 我方不会默默以报价为准,
那是一次零报错的金额事故(你以为下的是 5000,成交的是 500)。
⚠ 报价过期一律拒(state_invalid),不会回落成按新价成交。
重新报一次价、拿一个新的 quote_id。
⚠ 一份报价只成交一次。 用过的 quote_id 再拿来只会拿回原来那一单
(200 + duplicated: true),不会成交第二次 —— 无论你换没换幂等键。
⚠ 认不出的 quote_id(不存在 / 不是这个会员的 / 不是你名下的 /
不是经开放 API 锁的)一律 404 resource_not_found,四种同一响应。
⚠ 两侧资产都由你托管(M3 模型):这笔兑换是你自己重新配置了
你持有的资产,我方的池子一分不动。我方只从你的预付里收手续费。
预付不足时你的会员看到的是 service_unavailable(一句不解释原因的
「暂时不可用」)—— 会员不该知道「你的商户没钱了」。
⚠ 需要强认证的方向在开放 API 上一律拒(返回 step_up_required),
不是「跳过」。强认证走我方托管屏,我方绝不接受你在请求体里自证。
⚠ 这个端点不发 exchange.order.executed webhook。 那条事件只在
「会员自己在 App 里换」的那一侧发出;经开放 API 成交的单,成交结果
就在这次的 201 响应里。别在这里等一条不会来的事件。
前置条件
- 该方向已启用且未开强认证要求(开了的话开放 API 一律拒)
- 会员该资产可用余额 ≥ from_amount
- 你的预付账户在该资产上够付我方那笔手续费
请求头
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
x-on-behalf-of |
string | 必填 | 代哪个会员成交。归属由令牌与会员推导,不接受请求体声明。 |
x-idempotency-key |
string | 必填 | UUID。必填,且同一把键 24 小时内重放拿回同一张单。
换了请求体还用同一把键 → 409 idempotency_key_reused。 |
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
quote_id |
string | 可选 | POST /v1/exchange/quotes + lock: true 拿到的锁价凭据。
带不带 exc_ 前缀都认。
它就是这一单的订单号 —— 成交后响应里的 id 与它逐字符
相同,GET /v1/exchange/orders/{id} 也认它。所以这次请求
超时时先用它查一次,别盲目重投。 |
from_asset |
string | 可选 | 源资产代码,如 USDT。大小写不敏感。
带 quote_id 时可省;传了必须与报价一致。 |
to_asset |
string | 可选 | 目标资产代码,如 USD。
带 quote_id 时可省;传了必须与报价一致。 |
from_amount |
string | 可选 | 源资产扣减数量。字符串定点,位数 = from_asset 的
ledger_scale。这是扣多少,不是「换到多少」——
没有反向下单(指定 to_amount)的形态。
带 quote_id 时可省;传了必须与报价逐位相等。USDT 是 6 位,形如 500.000000 |
响应
duplicated: true,其余字段与 201 相同。{
"id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45",
"status": "executed",
"from_asset": "USDT",
"to_asset": "USD",
"from_amount": "500.000000",
"to_amount": "498.750000",
"rate": "1.0000",
"duplicated": true
}status 恒为 executed(这条线没有中间态:
要么成交,要么这次请求失败,不存在「处理中」)。
金额是定点十进制串。{
"id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45",
"status": "executed",
"from_asset": "USDT",
"to_asset": "USD",
"from_amount": "500.000000",
"to_amount": "498.750000",
"rate": "1.0000"
}invalid_request · invalid_fields quote_id
不是串,或 from_asset / to_asset / from_amount 与那份报价
对不上 · step_up_required 该方向要求强认证,开放 API 不开放
这一档(锁价之后被打开也一样拒 —— 授权判据取成交那一刻的)·
state_invalid 报价已过期 / 这一单已被取消或已失败 ·
request_rejected 冻结 / 销户 / 风控 ·
service_unavailable 你的预付不足或这条线不可用 ·
idempotency_key_required · idempotency_key_invalid 不是 UUID ·
member_context_required · member_not_found
⚠ 还落在 500 api_error 的业务拒绝:余额不足 · 超日限额 ·
低于起兑 / 高于上限 · 方向未启用 · 实名不足。
这些重试不会成功,别照着 500 无限重投。resource_not_found —— quote_id 认不出:不存在 / 不是这个会员的 /
不是你名下的 / 不是经开放 API 锁的。四种同一响应:分开就等于
给了一个报价单探测接口。idempotency_key_reused 同一把键配了不同的请求体 ·
idempotency_in_progress 上一次同键请求还在处理中(稍后重试)会触发的事件
绿 = 终局且是好消息 · 红 = 终局且要处置 · 紫 = 中间态。点进去看事件体与验签。
curl -X POST 'https://api.zise.com/v1/exchange/orders' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-on-behalf-of: $MEMBER_ID' \
-H 'x-idempotency-key: $IDEMPOTENCY_KEY' \
-H 'content-type: application/json' \
-d '{
"quote_id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45"
}'const res = await fetch("https://api.zise.com/v1/exchange/orders", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
body: JSON.stringify({
"quote_id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45"
}),
});
// 金额按字符串读,别让它变成 number
const data = await res.json();import requests
res = requests.post(
"https://api.zise.com/v1/exchange/orders",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-on-behalf-of": "$MEMBER_ID",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
json={
"quote_id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45"
},
)
# 金额用 Decimal(str(...)),不要 float
data = res.json()req, _ := http.NewRequest("POST", "https://api.zise.com/v1/exchange/orders",
strings.NewReader(`{
"quote_id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45"
}`))
req.Header.Set("x-auth-token", "Bearer $TOKEN")
req.Header.Set("x-on-behalf-of", "$MEMBER_ID")
req.Header.Set("x-idempotency-key", "$IDEMPOTENCY_KEY")
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
// 金额字段用 string 接,不要 float64HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.zise.com/v1/exchange/orders"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-on-behalf-of", "$MEMBER_ID")
.header("x-idempotency-key", "$IDEMPOTENCY_KEY")
.header("content-type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"quote_id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45"
}
"""))
.build();
// 金额字段用 String / BigDecimal,不要 double$ch = curl_init('https://api.zise.com/v1/exchange/orders');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
'x-on-behalf-of: $MEMBER_ID',
'x-idempotency-key: $IDEMPOTENCY_KEY',
'content-type: application/json',
],
CURLOPT_POSTFIELDS => <<<'JSON'
{
"quote_id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45"
}
JSON,
]);
$res = curl_exec($ch);
// 金额用 bcmath / 字符串,不要 floatval
{
"id": "exc_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45",
"status": "executed",
"from_asset": "USDT",
"to_asset": "USD",
"from_amount": "500.000000",
"to_amount": "498.750000",
"rate": "1.0000",
"duplicated": true
}