Z Zise Developers

兑换成交(原子 · 不可逆 · 无中间态)

POST /v1/exchange/orders scope: exchange:write
代会员调用 · 必带 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_idfrom_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_assetledger_scale。这是扣多少,不是「换到多少」—— 没有反向下单(指定 to_amount)的形态。 带 quote_id 时可省;传了必须与报价逐位相等。USDT 是 6 位,形如 500.000000

响应

200幂等命中(拿回原单,本次未成交)。响应体多一个 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
}
201已成交。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"
}
400已登记的对外码: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 无限重投。
404resource_not_found —— quote_id 认不出:不存在 / 不是这个会员的 / 不是你名下的 / 不是经开放 API 锁的。四种同一响应:分开就等于 给了一个报价单探测接口。
409idempotency_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 接,不要 float64
HttpRequest 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
200
{
  "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
}