快速开始
15 分钟跑通第一笔调用。
1 · 拿到凭据
在商户后台「开发者 → API Keys」创建一把 sandbox Key。创建时会一次性
返回三个值:
| 值 | 用途 | 我方是否保存 |
|---|---|---|
client_id | 换令牌时标识你是谁 | 是(常驻列表可见) |
api_key | 换令牌的密钥 | 只存哈希,丢了只能新建一把 |
signing_key | 请求签名的对称密钥 | 是(HMAC 是对称的,单向哈希验不了签名) |
live 环境的 Key 必须配 IP 白名单(强制非空)。
2 · 换取访问令牌
POST https://api-sandbox.zise.com/v1/connect/token
x-client-id: zc_test_7f3a9b2e4c1d
x-api-key: sk_test_••••••••••••
→ 200
{
"auth_token": "eyJhbGciOiJIUzI1NiIs…",
"expired_at": 1754872200,
"token_type": "Bearer",
"scopes": ["members:write","members:read", …]
}
有效期 30 分钟。多令牌可并存 —— 你的多个进程各换各的,不会互相踢掉
(这一点与某些同类平台相反,那个行为在多进程侧是个坑)。
签发限流 20 次/分钟:令牌该被缓存复用,高频换取本身是接入错误的信号。
3 · 给写请求签名
签名串五段,换行连接,顺序不可换:
POST
/v1/members
1754870400
2b7e1516-…-0f3c
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
- 第二段是 PATH + query string;
- 第五段是
hex(sha256(rawBody)),空 body 用上面那个定值。
x-signature = base64(HMAC-SHA256(signing_key, 签名串))
三件容易踩的事:
- 用 raw body,不要重新序列化。 你的 HTTP 库如果在发送前重排了 JSON
键序,签名会恒不匹配 —— 而报错长得像「密钥配错了」。
x-nonce5 分钟内不可二用。 少了它,签名只防篡改不防重放,
而一笔重放的付款就是第二笔真实付款。
x-timestamp容差 ±300 秒。 服务器时钟要对。
4 · 建一个会员
POST /v1/members
x-auth-token: Bearer eyJ…
x-idempotency-key: 6f1c2d80-…-9a4e ← UUID v4,写请求必带
x-timestamp / x-nonce / x-signature
{ "external_member_id": "u_88213", "email": "a@example.com" }
→ 201
{ "id": "mem_…", "external_member_id": "u_88213", "kyc_level": 0, … }
external_member_id 是你自己体系里的用户 ID —— 之后所有会员作用域的
调用都用它切入(x-on-behalf-of: u_88213)。
5 · 上报一笔入金
你在自己的托管侧收到链上转账之后,调这个接口让会员余额增加:
POST /v1/deposits
x-on-behalf-of: u_88213
{ "asset": "USDT", "amount": "100.000000", "reference": "0xabc…" }
reference 是你的业务流水号(通常是链上交易哈希)。它进幂等键 ——
重复上报同一笔时我方返回 replayed: true 而不会记第二笔余额。
6 · 接 Webhook
在商户后台配一个 https 端点。我方推送时带:
z-signature: t=<unix秒>,v1=<base64>
v1 = HMAC-SHA256(webhook_secret, "<t>." + rawBody)
四件必须做的事:
1. 校验签名,并检查 t 在 ±300 秒内(防重放);
2. 按 event_id 幂等 —— 我方保证至少一次,不保证恰好一次;
3. 按 status_version 向前合并 —— 收到倒序事件应丢弃。少了它,
一次慢响应会把「已到账」打回「处理中」;
4. 任意 2xx 即算成功,响应体我方不解析。超时 10 秒。
失败退避 5 次(1min / 5min / 30min / 2h / 6h),之后转 dead 并在商户后台
告警。不会静默丢弃。
上线前
去看「Go-live 检查清单」。其中最容易漏的一条:沙盒配额不高于生产
(我方两边取生产值),所以沙盒压测通过 = 生产也不会被限流。
参考 SDK
两份单文件、零/极少依赖的客户端随文档一起分发:
| 语言 | 文件 | 依赖 |
|---|---|---|
| Node | sdk/node/zise.mjs | 无(Node 18+ 内置 fetch + node:crypto) |
| Python | sdk/python/zise_client.py | 只有 requests |
各自目录下的 README.md 有完整用法。它们不是官方 SDK,不发布到
npm / PyPI、不承诺向后兼容 —— 行为与本文档冲突时以本文档为准。
建议直接把文件复制进你的工程再按需改。
之所以值得用它们起步:接入期真正出事故的是签名与重试,而这两件事的失败
形态都不报错。SDK 里封死了六件容易写反的:
1. 换令牌单飞 —— 首屏那几个并发不做单飞会各换一次,
而签发限流是 20 次/分钟,八并发刷三次就把自己限流了;
2. 每次投递重新签名(nonce 换新、时间戳刷新),而幂等键原封不动——
沿用旧签名头是 nonce_reused,换新幂等键是第二笔真实付款;
3. 「重试同一笔」与「发起新的一笔」在 API 上是同一个头,SDK 用两个不同的
动作表达(client.post() 生成新键 / call.send() 沿用同键);
4. 只对 504 与网络层失败自动重试,4xx 一律不重试 ——
写反了就是对一个永远不会成功的请求无限重试,把限流配额也一起烧光;
5. 金额一律留在字符串里,运算走 BigInt / Decimal(str(x));
6. 错误对象带 code 与 request_id,让你按 code 分支而不是按 HTTP 码。
5 分钟跑通第一个调用
下面这段不依赖任何东西(Node 18+ 直接 node first-call.mjs),
把整条链走一遍:换令牌 → 签名 → 建会员。看懂它就看懂了全部三段。
// first-call.mjs
import { createHash, createHmac, randomUUID } from "node:crypto";
const BASE = "https://api-sandbox.zise.com"; // ⚠ 环境判据是域名,不是请求头
const CLIENT_ID = process.env.ZISE_CLIENT_ID;
const API_KEY = process.env.ZISE_API_KEY; // 换令牌用
const SIGNING_KEY = process.env.ZISE_SIGNING_KEY; // 验签用(是另一个值)
// ① 换令牌(30 分钟,缓存复用;这是整条链上唯一不带令牌的端点)
const tokRes = await fetch(`${BASE}/v1/connect/token`, {
method: "POST",
headers: { "x-client-id": CLIENT_ID, "x-api-key": API_KEY },
});
const tok = await tokRes.json();
if (!tokRes.ok) throw new Error(`${tok.code}: ${tok.message}`);
console.log("token 到期于", new Date(tok.expired_at * 1000).toISOString());
// ② 签名。⚠ 序列化「只做一次」—— 哈希与发送必须是同一串字节,
// 否则键顺序一变签名恒不匹配,而报错长得像「密钥配错了」。
const path = "/v1/members";
const rawBody = JSON.stringify({ external_member_id: "u_88213", email: "u88213@example.com" });
const timestamp = String(Math.floor(Date.now() / 1000)); // 秒,不是毫秒
const nonce = randomUUID(); // 每次换新
const bodyHash = createHash("sha256").update(rawBody, "utf8").digest("hex");
const signature = createHmac("sha256", SIGNING_KEY)
// 五段,换行连接,顺序不可换。第二段含 query(这里没有)。
.update([ "POST", path, timestamp, nonce, bodyHash ].join("\n"), "utf8")
.digest("base64"); // ⚠ 是 base64,不是 hex
// ③ 调业务
const res = await fetch(BASE + path, {
method: "POST",
headers: {
"x-auth-token": `Bearer ${tok.auth_token}`,
"content-type": "application/json",
"x-idempotency-key": randomUUID(), // 写请求必带;重试要沿用「同一把」
"x-timestamp": timestamp,
"x-nonce": nonce,
"x-signature": signature,
},
body: rawBody, // 与算哈希的是同一个字符串
});
const out = await res.json();
console.log(res.status, out.id ?? `${out.code}: ${out.message}`, "req", res.headers.get("X-Request-Id"));
跑之前先在商户后台「开发者 → API Keys」建一把 sandbox Key,把三个值
放进环境变量。api_key 我方只存哈希,关掉创建页面就取不回来了。
调不通时按这个顺序查:environment_mismatch = 沙盒 Key 打到了生产域名;
timestamp_out_of_range = 机器时钟漂了;invalid_signature = 九成是
body 被重新序列化过,或者签名写成了 hex。
用 SDK 的话,上面整段就是这三行:
import { ZiseClient } from "./sdk/node/zise.mjs";
const zise = new ZiseClient({ baseUrl: BASE, clientId: CLIENT_ID, apiKey: API_KEY, signingKey: SIGNING_KEY });
const r = await zise.post("/v1/members", { external_member_id: "u_88213", email: "u88213@example.com" });
from zise_client import ZiseClient
zise = ZiseClient(BASE, CLIENT_ID, API_KEY, SIGNING_KEY)
r = zise.post("/v1/members", {"external_member_id": "u_88213", "email": "u88213@example.com"})