预付资产内部调剂(原子 · 不可逆 · 无中间态)
x-idempotency-key
动钱 · 你的预付:扣 from 资产 / 增 to 资产(同一名下,逐资产各自平)
失败处置见下方响应表。超时(504)用同一把幂等键重试——我方可能已经处理完;业务失败要换新键,同键会原样返回那次失败。
把你的一种预付资产换成另一种。只动你自己的备付金 ——
会员的余额、你的托管声明(custody)一分都不碰。
原子、不可逆、无中间态:要么这一次请求就成交了,要么它失败。
没有「处理中」,也没有撤销、没有追回、没有重试 —— 错账只能走
我方资金管理的双人审批冲正。所以下单前把 min_to_amount 想清楚。
幂等有两层,行为不同
- 24 小时内同键 + 同一份 body:我方把首次那份响应原样回放
(201、逐字相同的 body,另带 X-Idempotent-Replay: true 响应头),
处理器一行都不执行。
- 超过 24 小时那把键被当成新请求,但资金层仍然认得它
(库级唯一约束,没有 TTL):处理器会拿回原来那一单并回
200 + duplicated: true。这一档下同样没有第二次成交。
⚠ 所以 duplicated 只在第二种情况下出现:正常成交与 24 小时内的
回放都没有这个键。if (res.duplicated === false) 永远为假,请判真值;
要判「这次是不是重放」,最稳的是 X-Idempotent-Replay 响应头。
⚠ 拿到 service_unavailable 时用同一把键重试是被支持的 ——
我方不缓存这一档 400(它会因为你充值预付、或我方总闸恢复而自行解除)。
其余业务失败会被原样回放,那几种要换新键。
四个会当天返工的点
- ⚠ 传
quote_id一律 400quote_lock_unsupported,不是静默忽略。
静默忽略的后果是「你以为锁了价而实际没有」,差额由你自己吃且全程
零提示。要防价格移动用 min_to_amount。
- ⚠ 滑点没达到时回的是
amount_out_of_range,不是一个专门的滑点码。
同一个码还覆盖「低于起兑额」「高于单笔上限」「金额非正」——
想在自己这边分开,先读 GET /v1/merchant/conversion-pairs 的限额。
- ⚠ 余额闸减掉了已排队额。 排在
pending_merchant_funds里的订单
已经把那部分预付许诺出去了(只是还没冻)。所以你会在「余额看起来
够」的情况下拿到 merchant_insufficient_funds —— 那是对的,
不减的话你一次「把 USDT 全换成 USDC」会让那一队订单永远推不动。
去看 GET /v1/merchant/pending。
- ⚠
product_not_available不等于「暂时不支持」。 只有托管模型是
merchant_hosted 的主体才有预付池;平台自营主体上「换你的预付」
这句话不成立。方向未启用、方向不在价目表里也归这个码。
定价取我方价目表那一行,不是你给你的会员定的零售价 ——
这条路径上我方是真实的对手方,拿零售价成交等于让对手方自己报价。
手续费落在 fee_side 那一侧的资产上,出参 fee_asset / fee_amount
明说是哪一侧。
金额一律十进制串。from_amount 的小数位不得超过 from_asset 的
ledger_scale(超出的尾数全为 0 时放行),否则 400 invalid_request。
同币种方向(from_asset == to_asset)同样是 400 invalid_request。
⚠ 这个端点不发任何 Webhook。 成交结果就在这次响应里,
别在这里等一条不会来的事件。
前置条件
- 你的
custody_model是merchant_hosted(自营主体没有预付池) - 该方向在我方价目表里存在且已启用
- 两个资产都已启用,且都在你的资产白名单内(没配白名单 = 不限制)
- 该资产可用余额 减去已排队额 ≥
from_amount
请求头
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
x-idempotency-key |
string | 必填 | UUID v4。必填;同键 24 小时内重放拿回同一单。
换了请求体还用同一把键 → 409 idempotency_key_reused。 |
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
from_asset |
string | 必填 | 付出的资产代码,大小写不敏感(服务端转大写) |
to_asset |
string | 必填 | 换到的资产代码,大小写不敏感。与 from_asset 相同一律 400 |
from_amount |
string | 必填 | 付出多少。十进制串,小数位不得超过 from_asset 的
ledger_scale。必须为正。USDT(scale 6):"5000.000000" |
min_to_amount |
string | 可选 | 滑点护栏:实收低于它就整笔拒(amount_out_of_range),
判在过账之前。省略 = 不设护栏,按成交那一刻的价成交。
位数按 to_asset 的 ledger_scale。 |
quote_id |
string | 可选 | ⚠ 不接受。请求体里出现这个键就拒(含 "" 与 null),
回 400 quote_lock_unsupported —— 判据是
quote_id !== undefined,不是「值非空」。
不用它就整个字段不要发。
⚠ 这一条专门写给用强类型 SDK 或固定模板序列化请求体的接入方:
把未使用的字段发成 "" / null 是很自然的写法,
而那样每一笔都会失败,错误信息还指向一个你根本没用的功能。
本端点在成交那一刻现算价,锁价能力没有就是没有。 |
响应
duplicated: true,其余字段与首次逐字相同。
(24 小时内的重放走的是另一条路 —— 原样回放那份 201,见上。)ledger_scale_from / ledger_scale_to。{
"id": "mcv_7a1e5c30-2b44-4c11-9f8e-31d0a7b62c45",
"from_asset": "USDT",
"to_asset": "USDC",
"from_amount": "5000.000000",
"to_amount": "4990.003750",
"rate": "0.999500",
"fee_asset": "USDC",
"fee_amount": "7.496250",
"ledger_scale_from": 6,
"ledger_scale_to": 6,
"created_at": "2026-08-13T04:12:55.108Z"
}quote_lock_unsupported 传了 quote_id ·
invalid_request body 非 JSON / 缺资产 / 同币种 / 金额格式或精度非法 ·
asset_not_allowed 资产已下架,或不在你的白名单里 ·
product_not_available 这个方向没开,或你的托管模型没有预付池 ·
amount_out_of_range 低于起兑额 / 高于单笔上限 / 滑点护栏没达到 ·
merchant_insufficient_funds 可用余额减去已排队额不够 ·
limit_exceeded 撞日累计金额或笔数上限(按方向计,阈值见价目表;
⚠ 这一个不带 limit_type / limit_scope)·
service_unavailable 我方兑换总闸关闭、报不出价、或你这个资产的
账户处于非正常处置态insufficient_scope 缺 merchant:write ——
⚠ 这把 scope 是受限的,须我方单独审批才能授出,
勾了不等于拿到了。只读 Key 在这里恒 403。idempotency_key_reused 同键不同 body · idempotency_in_progresscurl -X POST 'https://api.zise.com/v1/merchant/conversions' \
-H 'x-auth-token: Bearer $TOKEN' \
-H 'x-idempotency-key: $IDEMPOTENCY_KEY' \
-H 'content-type: application/json' \
-d '{
"from_asset": "USDT",
"to_asset": "USDC",
"from_amount": "5000.000000",
"min_to_amount": "4990.000000"
}'const res = await fetch("https://api.zise.com/v1/merchant/conversions", {
method: "POST",
headers: {
"x-auth-token": "Bearer $TOKEN",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
body: JSON.stringify({
"from_asset": "USDT",
"to_asset": "USDC",
"from_amount": "5000.000000",
"min_to_amount": "4990.000000"
}),
});
// 金额按字符串读,别让它变成 number
const data = await res.json();import requests
res = requests.post(
"https://api.zise.com/v1/merchant/conversions",
headers={
"x-auth-token": "Bearer $TOKEN",
"x-idempotency-key": "$IDEMPOTENCY_KEY",
"content-type": "application/json",
},
json={
"from_asset": "USDT",
"to_asset": "USDC",
"from_amount": "5000.000000",
"min_to_amount": "4990.000000"
},
)
# 金额用 Decimal(str(...)),不要 float
data = res.json()req, _ := http.NewRequest("POST", "https://api.zise.com/v1/merchant/conversions",
strings.NewReader(`{
"from_asset": "USDT",
"to_asset": "USDC",
"from_amount": "5000.000000",
"min_to_amount": "4990.000000"
}`))
req.Header.Set("x-auth-token", "Bearer $TOKEN")
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/merchant/conversions"))
.header("x-auth-token", "Bearer $TOKEN")
.header("x-idempotency-key", "$IDEMPOTENCY_KEY")
.header("content-type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"from_asset": "USDT",
"to_asset": "USDC",
"from_amount": "5000.000000",
"min_to_amount": "4990.000000"
}
"""))
.build();
// 金额字段用 String / BigDecimal,不要 double$ch = curl_init('https://api.zise.com/v1/merchant/conversions');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'x-auth-token: Bearer $TOKEN',
'x-idempotency-key: $IDEMPOTENCY_KEY',
'content-type: application/json',
],
CURLOPT_POSTFIELDS => <<<'JSON'
{
"from_asset": "USDT",
"to_asset": "USDC",
"from_amount": "5000.000000",
"min_to_amount": "4990.000000"
}
JSON,
]);
$res = curl_exec($ch);
// 金额用 bcmath / 字符串,不要 floatval
// spec 里还没有这个 operation 的响应示例