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),不会成交第二次。

⚠ 成交那一刻会重新报一次价,不引用 POST /v1/exchange/quotes

的结果(那个端点也没有发出可引用的 id)。所以你拿到的成交价可能与

几秒前的报价不同 —— 要给终端用户一屏确认的话,把那一屏的有效期

做得比 quote_ttl_sec 短。

两侧资产都由你托管(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

请求体

字段类型必填说明
from_asset string 必填 源资产代码,如 USDT。大小写不敏感。
to_asset string 必填 目标资产代码,如 USD
from_amount string 必填 源资产扣减数量。字符串定点,位数 = from_assetledger_scale。这是扣多少,不是「换到多少」—— 没有反向下单(指定 to_amount)的形态。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 · step_up_required 该方向要求 强认证,开放 API 不开放这一档 · request_rejected 冻结 / 销户 / 风控 · service_unavailable 你的预付不足或这条线不可用 · idempotency_key_required · idempotency_key_invalid 不是 UUID · member_context_required · member_not_found还落在 500 api_error 的业务拒绝:余额不足 · 超日限额 · 低于起兑 / 高于上限 · 方向未启用 · 实名不足 · 报价过期 · 这一单已被取消或已失败。这些重试不会成功,别照着 500 无限重投。
409idempotency_key_reused 同一把键配了不同的请求体 · idempotency_in_progress 上一次同键请求还在处理中(稍后重试)
调用样例
curl -X POST 'https://api.zise.com/v1/exchange/orders' \
  -H 'authorization: Bearer $TOKEN' \
  -H 'x-zise-merchant: $MERCHANT_ID' \
  -H 'x-on-behalf-of: $MEMBER_ID' \
  -H 'x-idempotency-key: $(uuidgen)' \
  -H 'content-type: application/json' \
  -d '{
    "from_asset": "USDT",
    "to_asset": "USD",
    "from_amount": "500.000000"
  }'
const res = await fetch(
  "https://api.zise.com/v1/exchange/orders",
  {
    method: "POST",
    headers: {
      "authorization": "Bearer $TOKEN",
      "x-zise-merchant": "MERCHANT_ID",
      "x-on-behalf-of": "MEMBER_ID",
      "x-idempotency-key": "crypto.randomUUID()",
      "content-type": "application/json"
    },
    body: JSON.stringify({
      "from_asset": "USDT",
      "to_asset": "USD",
      "from_amount": "500.000000"
    }),
  },
);
// 金额一律按字符串读,不要 JSON.parse 成 number
const data = await res.json();
import requests

res = requests.post(
    "https://api.zise.com/v1/exchange/orders",
    headers={
        "authorization": "Bearer $TOKEN",
        "x-zise-merchant": "$MERCHANT_ID",
        "x-on-behalf-of": "$MEMBER_ID",
        "x-idempotency-key": "$(uuidgen)",
        "content-type": "application/json"
    },
    json={
        "from_asset": "USDT",
        "to_asset": "USD",
        "from_amount": "500.000000"
    },
)
# 金额用 Decimal(str(...)),不要 float
data = res.json()