From receiving your API Key to your first real call in five minutes.
Quickstart
These values stay in your browser and are not sent to us. This is a static page.
Make your first call in 15 minutes.
1 · Obtain Credentials
Create an API Key under “Developer → API Keys” in the merchant portal. Creation returns three values once only:
| Value | Purpose | Do we retain it? |
|---|---|---|
client_id | Identifies you when obtaining a token | Yes (visible in the persistent list) |
api_key | Secret for obtaining tokens | Hash only; create a new key if lost |
signing_key | Symmetric key for request signing | Yes (HMAC is symmetric; a one-way hash cannot verify signatures) |
Live keys require an IP allowlist (it cannot be empty).
2 · Obtain an Access Token
POST https://api.zinfra.dev/v1/connect/token
x-client-id: zc_7f3a9b2e4c1d
x-api-key: sk_••••••••••••
→ 200
{
"auth_token": "eyJhbGciOiJIUzI1NiIs…",
"expired_at": 1754872200,
"token_type": "Bearer",
"scopes": ["members:write","members:read", …]
}
Valid for 30 minutes. Multiple tokens can coexist—separate processes can obtain their own without invalidating each other (unlike some comparable platforms, where that behavior causes multiprocess problems).
Issuance is limited to 20 per minute. Cache and reuse tokens; frequent issuance itself signals an integration error.
3 · Sign Write Requests
The signature string has five segments, joined by newlines in this exact order:
POST
/v1/members
1754870400
2b7e1516-…-0f3c
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
- Segment two is PATH + query string;
- Segment five is
hex(sha256(rawBody)); use the constant above for an empty body.
x-signature = base64(HMAC-SHA256(signing_key, 签名串))
Three common pitfalls:
- Use the raw body; do not serialize it again. If your HTTP library reorders JSON keys before sending, signatures will always fail, with an error that looks like a misconfigured key.
- Do not reuse
x-noncewithin 5 minutes. Without it, signatures prevent tampering but not replay, and a replayed payment is a second real payment. x-timestamptolerance is ±300 seconds. Keep your server clock accurate.
4 · Create a Member
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 is your own system's user ID. Use it for all subsequent member-scoped calls (x-on-behalf-of: u_88213).
5 · Report a Deposit
After receiving an on-chain transfer in your own custody system, call this endpoint to increase the member's balance:
POST /v1/deposits
x-on-behalf-of: u_88213
{ "asset": "USDT", "amount": "100.000000", "reference": "0xabc…" }
reference is your business transaction reference (usually the on-chain transaction hash). It participates in idempotency— reporting the same deposit again returns replayed: true and does not credit it twice.
6 · Integrate Webhooks
Configure an https endpoint in the merchant portal. Our deliveries include:
z-signature: t=<unix秒>,v1=<base64>
v1 = HMAC-SHA256(webhook_secret, "<t>." + rawBody)
Four required actions:
- Verify the signature and ensure
tis within ±300 seconds (replay protection); - Deduplicate by
event_id—delivery is at least once, not exactly once; - Merge forward by
status_version—discard out-of-order events. Without this, a slow response can turn “received” back into “processing”; - Any 2xx means success; we do not parse the response body. Timeout is 10 seconds.
Failures are retried five times with backoff (1min / 5min / 30min / 2h / 6h), then marked dead and reported in the merchant portal. They are never silently discarded.
Before Going Live
Read the “Go-live Checklist.” A frequently missed point: sandbox quotas are no higher than production (both use production values), so passing sandbox load tests means production will not rate-limit that load either.
Reference SDKs
Two single-file clients with zero or minimal dependencies are distributed with the documentation:
| Language | File | Dependencies |
|---|---|---|
| Node | sdk/node/zise.mjs | None (Node 18+ built-in fetch + node:crypto) |
| Python | sdk/python/zise_client.py | Only requests |
Each directory's README.md contains complete usage instructions. They are not official SDKs, are not published to npm / PyPI, and carry no backward-compatibility guarantee. This documentation takes precedence if behavior differs. Copy the files into your project and adapt them as needed.
They are useful starting points because signing and retries cause real integration incidents, and both can fail without obvious errors. The SDKs enforce six commonly reversed rules:
- Single-flight token acquisition—without it, concurrent first-screen requests each obtain a token, and a 20-per-minute issuance limit means three refreshes with eight concurrent calls rate-limit you;
- Sign every delivery again (new nonce and timestamp), while retaining the exact idempotency key— old signature headers cause
nonce_reused; a new idempotency key creates a second real payment; - “Retry the same operation” and “start a new operation” use the same API header, but the SDK exposes separate actions (
client.post()creates a new key /call.send()retains the same key); - Automatically retry only 504 and network failures, never 4xx— reversing this causes endless retries of a request that cannot succeed, exhausting the rate limit too;
- Keep all amounts as strings; calculate using BigInt /
Decimal(str(x)); - Error objects include
codeandrequest_id, so you branch oncoderather than HTTP status.
Your First Call in 5 Minutes
The following has no dependencies (run node first-call.mjs directly on Node 18+), and walks the entire chain: obtain token → sign → create member. Understanding it covers all three stages.
// first-call.mjs
import { createHash, createHmac, randomUUID } from "node:crypto";
const BASE = "https://api.zinfra.dev"; // ⚠ 环境判据是域名,不是请求头
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"));
Before running, create an API Key under “Developer → API Keys” in the merchant portal and place the three values in environment variables. We store only the hash of api_key; it cannot be retrieved after closing the creation page.
If the call fails, check in this order: invalid_credentials = wrong credentials or credentials from another deployment environment; timestamp_out_of_range = clock drift; invalid_signature = usually (90%) a reserialized body or a signature encoded as hex.
With the SDK, the entire example above becomes these three lines:
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"})