身份鉴权
每个需要鉴权的 API 请求都必须使用您的 API 密钥私钥(Secret)进行签名。私钥本身绝不在网络上传输:服务端使用相同的参数重新计算签名并比对验证。
开始之前
| 凭证类型 | 格式示例 | 适用场景 | 有效期 |
|---|---|---|---|
| API Key + Secret | ak_live_… / sk_live_…(Nile 测试网为 ak_test_… / sk_test_…) | 后端发起的每次 API 请求 | 永久有效,直到主动撤销或到达 expires_at |
| Bootstrap token | Authorization: Bearer abt_… | 首次注册创建账户、查询充值地址、GET /v1/account 以及创建第一个 API Key —— 仅限这些操作 | 15 分钟 |
| 无凭证(公开) | — | GET /v1/prices、GET /v1/estimate、GET /v1/orderbook、GET /v1/resources/{address} 以及注册挑战挑战字串 | 按源 IP 频控限制 |
API 密钥是机器凭证:切勿在前端网页中暴露使用。在首次充值前即可创建密钥,以便您的程序自动获取充值地址(GET /v1/deposit-addresses)并自主充值;在账户余额充足以支付订单前,下单请求将返回 4001 insufficient_funds。密钥的 Secret 仅在创建时展示一次,请妥善保存。
三个请求头
| 请求头 | 取值要求 |
|---|---|
X-API-KEY | API 密钥 ID,与控制台展示的一致。非机密。 |
X-API-TIMESTAMP | 当前 UTC 时间,采用精确到毫秒的 ISO 8601 格式,例如 2026-09-11T18:04:05.123Z。 |
X-API-SIGN | base64(HMAC_SHA256(api_secret, canonical_string)) |
规范字符串 (Canonical String)
canonical_string = timestamp + METHOD + path + query + body
| 组成部分 | 严格要求 |
|---|---|
timestamp | X-API-TIMESTAMP 的完整字符串值,逐字节完全一致。 |
METHOD | 全大写 HTTP 方法:GET、POST、PATCH、DELETE。 |
path | 包含 /v1 前缀的请求路径,必须与实际发送的 URL 编码一致,例如 /v1/orders。 |
query | 无查询参数时为 ""(空字符串);有参数时必须包含开头的 ? 以及与网络请求完全相同的原始查询字符串 —— 切勿重新排序或重复编码。 |
body | UTF-8 编码的原始请求体字节串;无请求体(如 GET 请求)时为 ""(空字符串)。 |
各部分之间没有任何分隔符。请确保请求体序列化一次后,用完全相同的字节串同时用于签名和网络传输:使用格式化缩进(pretty-print)的 JSON 进行签名,却发送紧凑压缩(compact)的 JSON,是导致 1002 invalid_signature 最常见的原因。
步骤 1 — 构造并签名字符串
以时间 2026-09-11T18:04:05.123Z、密钥私钥 sk_test_example 对 GET /v1/balance 进行签名:
2026-09-11T18:04:05.123ZGET/v1/balance
TS=2026-09-11T18:04:05.123Z
printf '%s' "${TS}GET/v1/balance" | openssl dgst -sha256 -hmac "sk_test_example" -binary | base64
import { createHmac } from "node:crypto";
const ts = "2026-09-11T18:04:05.123Z";
const sign = createHmac("sha256", "sk_test_example").update(`${ts}GET/v1/balance`).digest("base64");
console.log(sign);
import base64, hashlib, hmac
ts = "2026-09-11T18:04:05.123Z"
digest = hmac.new(b"sk_test_example", f"{ts}GET/v1/balance".encode(), hashlib.sha256).digest()
print(base64.b64encode(digest).decode())
上述三种方式均输出 zqttXJ133TRJ7aj14dJ3jamfeCG2mDj+TbNPrrBJl2g=。在排查任何其他错误之前,请先比对您的程序是否生成相同结果。
步骤 2 — 发送请求
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
SIGN=$(printf '%s' "${TS}GET/v1/balance" | openssl dgst -sha256 -hmac "$TENERGY_SECRET" -binary | base64)
curl -s https://api-nile.tenergy.me/v1/balance \
-H "X-API-KEY: $TENERGY_KEY" -H "X-API-TIMESTAMP: $TS" -H "X-API-SIGN: $SIGN"
import { createHmac } from "node:crypto";
const ts = new Date().toISOString();
const sign = createHmac("sha256", process.env.TENERGY_SECRET!)
.update(`${ts}GET/v1/balance`)
.digest("base64");
const res = await fetch("https://api-nile.tenergy.me/v1/balance", {
headers: { "X-API-KEY": process.env.TENERGY_KEY!, "X-API-TIMESTAMP": ts, "X-API-SIGN": sign },
});
console.log(res.status, await res.json());
import base64, hashlib, hmac, json, os, urllib.error, urllib.request
from datetime import datetime, timezone
ts = datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
sign = base64.b64encode(hmac.new(os.environ["TENERGY_SECRET"].encode(),
f"{ts}GET/v1/balance".encode(), hashlib.sha256).digest()).decode()
req = urllib.request.Request("https://api-nile.tenergy.me/v1/balance", headers={
"X-API-KEY": os.environ["TENERGY_KEY"], "X-API-TIMESTAMP": ts, "X-API-SIGN": sign})
try:
with urllib.request.urlopen(req) as res:
print(res.status, json.load(res))
except urllib.error.HTTPError as err:
print(err.code, json.load(err))
快速上手指南 将这一签名逻辑封装成了通用的 tenergy(method, path, body) 辅助函数,方便直接调用。
时钟偏移、防重放与 IP 规则
| 规则 | 标准要求 | 违反时的错误响应 |
|---|---|---|
| 时钟偏移 (Clock skew) | 与服务器时钟相差不超过 ±5 秒 | 1003 signature_timestamp_skew —— details.server_time 将返回服务器标准时间 |
| 防重放 (Replay) | 在该时间窗口内,每个签名仅允许使用一次 | 1009 replayed_signature —— 每次请求(包括重试)都必须生成全新的时间戳和签名 |
| IP 白名单 | 单个密钥可选配置;留空表示允许任意 IP | 1004 ip_not_allowed —— details.source_ip 将返回我们识别到的来源 IP |
| 环境隔离 | 主网域名仅接受 ak_live_ 密钥,Nile 测试网仅接受 ak_test_ 密钥 | 在查询密钥之前即直接拒绝,返回 1012 key_environment_mismatch |
请在发起签名的服务器上开启 NTP 自动时间同步。时钟漂移会导致所有请求直接返回 1003;单纯扩大重试次数无法解决问题。
权限范围 (Scopes)
API 密钥只具备创建时显式勾选的权限范围 —— scopes 是必填字段,系统不存在默认权限集,也不会在任何地方进行预选。权限名称遵循标准的 area.action 命名规范(详见 openapi.yaml 中的 ApiKeyScope);API 密钥参考 详细列出了每个权限的具体作用。超出密钥权限的调用将返回 403 及 1005 insufficient_scope,并在 details.required_scope 中指出所缺失的权限名称。请仅申请业务实际需要的最小权限。
幂等性保证
| 请求接口 | 幂等控制字段 | 重复调用时的行为 |
|---|---|---|
POST /v1/orders | 请求体中的 client_order_id | 返回原始订单结果,HTTP 200,绝不重复扣费 |
| 其他写操作接口 | 请求头 Idempotency-Key,长度 8–128 字符 | 返回原始执行结果 |
幂等记录保留 24 小时。使用相同的幂等键但提交不同的请求体,将被系统拒绝并返回 3010 idempotency_conflict。