Webhooks — 订单通知
注册一个 HTTPS 端点,以便在您的订单完成并在链上确认的瞬间收到已签名的 Webhook。无需轮询,一旦收到通知即可继续您的业务流程(例如释放 USDT)。
我们会推送三种事件:
| 事件 | 发送时机 |
|---|---|
delegation.confirmed | 能量租赁(1h / 5m)在链上确认 |
bandwidth.delegated | 带宽订单已完成 |
activation.confirmed | 地址激活在链上执行完毕 |
本页面涵盖了管理 API(创建 / 列出 / 编辑 / 轮换密钥 / 删除您的端点)以及我们向您推送的 Webhook 格式。
ℹ️ 角色说明。 您在此处管理端点。订单验证通过后,Netts 会异步执行推送 — 无需进行轮询。系统仅发送成功事件;失败和超时订单绝不会推送。
🔒 我们发送的每个哈希都预先经过链上验证。 仅当 Webhook 中包含的每个交易哈希都在区块中被查询到后,才会派发通知。如果哈希尚未入块,推送将被挂起,并每 30 秒重新检查一次,最长持续 5 分钟;如果始终未上链,该订单将不发送任何内容。您绝不会收到链上不存在的哈希。
端点基础 URL
https://netts.io/apiv2/webhooks请求头
| 请求头 | 必填 | 描述 |
|---|---|---|
| Content-Type | 是 (针对 POST/PATCH) | application/json |
| X-API-KEY | 是 | 来自 Netts 控制面板的 API 密钥 |
| X-Real-IP | 是 | 来自白名单的 IP 地址 |
您的 user_id 是从 API 密钥派生的 — 无需手动传递。您只能查看和修改属于您自己的端点。
主端点与备用端点
您最多可以注册两个端点,且每个端点都有一个 role(角色):
| 角色 | 用途 |
|---|---|
primary | 每个 Webhook 默认推送的目标地址。 |
backup | 备用端点。仅当向 primary 推送失败且重试次数耗尽后才会使用。 |
单个已确认的订单仅生成一个 Webhook。这不是扇出式广播:同一个事件绝不会同时发送到两个地址。backup 端点的存在是为了提高可用性 — 如果您的主服务器不可达或持续返回非 2xx 响应,推送将转至备用端点,而不会被直接丢弃。
您创建的第一个端点将成为 primary,第二个将成为 backup。您可以显式传递 role,也可以稍后使用 PATCH 互换它们。
为什么不针对每种操作类型设置独立的 URL? 因为事件类型包含在请求体内部的
event字段中。一套处理程序、一次签名校验,无需注册任何新端点即可直接接收新增的事件类型。
管理端点
创建 — POST /apiv2/webhooks
注册一个新端点并返回仅显示一次的 secret(请妥善保存 — 它用于为您收到的每个 Webhook 进行签名)。
// request body — role is optional
{
"url": "https://your-server.example/netts/delegation-hook",
"role": "primary"
}如果省略 role,将按顺序分配首个空闲角色:先是 primary,然后是 backup。
// response 201
{
"detail": {
"code": 10000,
"status": "created",
"data": {
"id": 1,
"url": "https://your-server.example/netts/delegation-hook",
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"role": "primary",
"is_active": true,
"created_at": "2026-01-01T00:00:00"
}
}
}URL 要求(在创建和每次编辑时进行验证):
- 必须为
https; - 必须解析为公网地址 — 回环地址、私有地址(RFC1918)、链路本地地址(包括
169.254.169.254)以及其他不可路由网段均会被拒绝; - URL 中不得包含认证凭据(
user:pass@…); - 长度最多 2048 个字符。
被拒绝的 URL 将返回 400。
您最多可以拥有两个端点 — 一个 primary 和一个 backup。尝试创建第三个端点将返回 409 (4090)。申请已被占用的 role 将返回 409 (4091) — 请使用 PATCH 互换角色,或先删除现有端点。
curl -X POST https://netts.io/apiv2/webhooks \
-H "Content-Type: application/json" \
-H "X-API-KEY: your_api_key" \
-H "X-Real-IP: your_whitelisted_ip" \
-d '{"url": "https://your-server.example/netts/delegation-hook", "role": "primary"}'列表 — GET /apiv2/webhooks
返回您的端点列表(此处绝不返回 secret)。
{
"detail": {
"code": 10000,
"status": "ok",
"data": {
"endpoints": [
{
"id": 1,
"url": "https://your-server.example/netts/delegation-hook",
"role": "primary",
"is_active": true,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00"
},
{
"id": 2,
"url": "https://backup.example/netts/delegation-hook",
"role": "backup",
"is_active": true,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00"
}
],
"count": 2,
"max_endpoints": 2,
"roles": ["primary", "backup"]
}
}
}获取单个端点 — GET /apiv2/webhooks/{id}
格式与列表项相同(不包含 secret)。传入其他用户的或不存在的 id 将返回 404。
编辑 — PATCH /apiv2/webhooks/{id}
修改 url、is_active 和/或 role。可以仅传递其中任意子集;空请求体将返回 422。变更后的 url 会被重新验证(https / SSRF)。传入其他用户的或不存在的 id 将返回 404。
// request body (any subset)
{ "url": "https://your-server.example/netts/new-hook", "is_active": false }// response 200
{
"detail": {
"code": 10000,
"status": "updated",
"data": {
"id": 1,
"url": "https://your-server.example/netts/new-hook",
"role": "primary",
"is_active": false,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:01"
}
}
}将备用端点提升为主端点。 向您的备用端点发送 {"role": "primary"} 将在单次事务中互换这两个角色 — 原主端点将变为备用端点。您绝不会处于没有主端点的状态,也无需针对另一个端点发起单独调用。
curl -X PATCH https://netts.io/apiv2/webhooks/2 \
-H "Content-Type: application/json" \
-H "X-API-KEY: your_api_key" \
-H "X-Real-IP: your_whitelisted_ip" \
-d '{"role": "primary"}'设置
is_active: false可以在不删除端点的情况下暂停推送;设置为true可恢复。暂停您的primary端点并不会自动提升备用端点 — 推送目标仍将是主端点。如果您希望备用端点接管,请互换角色。
轮换密钥 — POST /apiv2/webhooks/{id}/rotate-secret
生成新的 secret 并仅返回一次。新密钥对后续推送立即生效 — 无需执行其他操作。每个端点都拥有独立的密钥:轮换主端点的密钥不会影响备用端点。
// response 200
{
"detail": {
"code": 10000,
"status": "rotated",
"data": {
"id": 1,
"secret": "whsec_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
}
}
}删除 — DELETE /apiv2/webhooks/{id}
硬删除该端点并释放其角色。返回 204(无响应体);传入其他用户的或不存在的 id 将返回 404。
curl -X DELETE https://netts.io/apiv2/webhooks/1 \
-H "X-API-KEY: your_api_key" -H "X-Real-IP: your_whitelisted_ip"我们推送的 Webhooks
当您的某个订单完成时,Netts 会向您的 primary 端点发送一个 POST 请求。每个请求体均为 application/json(UTF-8);地址和哈希始终为完整值。
所有事件共有的字段:
| 字段 | 类型 | 描述 |
|---|---|---|
event | string | 事件类型 — 用于您处理程序的路由键 |
delivery_id | int | 推送 ID — 供您在接收端使用的去重键。同时在 X-Netts-Delivery 请求头中发送。 |
order_id | string | 您的订单 ID |
order_type | string | 1h、5m、bandwidth 或 activation |
tx_hashes | string[] | 该操作的所有交易哈希,每个哈希均在链上验证过 |
confirmed_at | string | UTC ISO-8601 格式时间 |
delegation.confirmed — 能量租赁
{
"event": "delegation.confirmed",
"delivery_id": 1,
"order_id": "1Hxxxxxxxxxx",
"order_type": "1h",
"receive_address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"energy_amount": 65000,
"tx_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"tx_hashes": ["0000000000000000000000000000000000000000000000000000000000000000"],
"delegation_timestamp": 1700000000000,
"confirmed_at": "2026-01-01T00:00:00Z"
}| 字段 | 类型 | 描述 |
|---|---|---|
order_type | string | 1h 或 5m |
receive_address | string | 接收 Energy 的 TRON 地址 |
energy_amount | int | 质押代理的 Energy 数量 |
tx_hash | string | 历史遗留字段,保留以实现兼容性:与 tx_hashes[0] 相同 |
delegation_timestamp | int? | 可选 — 仅在通过 Mongo 路径确认时存在 |
在新集成中推荐优先使用
tx_hashes— 原则上一个订单可能由多个交易完成。tx_hash将继续保持可用。
bandwidth.delegated — 带宽订单
{
"event": "bandwidth.delegated",
"delivery_id": 2,
"order_id": "B1Hxxxxxxxxxxxxx",
"order_type": "bandwidth",
"rental_label": "1h",
"rental_seconds": 3600,
"receive_address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"bandwidth_amount": 400,
"fulfillment": "delegated",
"tx_hashes": ["0000000000000000000000000000000000000000000000000000000000000000"],
"confirmed_at": "2026-01-01T00:00:00Z"
}| 字段 | 类型 | 描述 |
|---|---|---|
rental_label / rental_seconds | string / int | 租赁时长,例如 1h / 3600 |
receive_address | string | 接收 Bandwidth 的 TRON 地址 |
bandwidth_amount | int | Bandwidth 单位数量(净值) |
fulfillment | string | 订单完成方式 — 详见下文 |
fulfillment 取值:
| 取值 | 含义 | tx_hashes |
|---|---|---|
delegated | 从我们的资金池中代理 Bandwidth | 1 个或多个哈希 |
trx_send | 通过向目标地址直接发送 TRX 代替代理来完成 | 1 个或多个哈希 |
already_enough | 该地址已有足够的空闲 Bandwidth — 未在链上发送任何内容 | 空 |
already_enough 是 tx_hashes 为空的唯一情况:订单已成功完结,但由于无需执行交易,因而没有生成交易。
activation.confirmed — 地址激活
{
"event": "activation.confirmed",
"delivery_id": 3,
"order_id": "123456",
"order_type": "activation",
"address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"activation_type": "ACC_CREATE",
"source": "telegram_bot",
"tx_hashes": ["0000000000000000000000000000000000000000000000000000000000000000"],
"confirmed_at": "2026-01-01T00:00:00Z"
}| 字段 | 类型 | 描述 |
|---|---|---|
order_id | string | 激活订单 ID(数字字符串) |
address | string | 被激活的 TRON 地址 |
activation_type | string | ACC_CREATE (AccountCreateContract) 或 DIRECT (TRX 转账) |
source | string | 来源标识。可能是一个服务标签,或者是需要该激活操作的 Energy 订单 ID |
仅推送真实的激活事件。 如果地址本身已经被激活且未执行任何链上交易,则完全不会发送 Webhook。
同时需要激活操作的 Energy 订单会生成两个 Webhook — 一个
activation.confirmed和一个delegation.confirmed。它们是具有独立delivery_id的不同事件;请根据event字段进行路由。
我们发送的请求头:
| 请求头 | 值 |
|---|---|
X-Netts-Event | 事件类型:delegation.confirmed、bandwidth.delegated 或 activation.confirmed |
X-Netts-Delivery | delivery_id (去重) |
X-Netts-Timestamp | 发送时的 Unix 秒级时间戳 |
X-Netts-Signature | sha256=<hex>,其中 hex = HMAC_SHA256(secret, "<timestamp>." + raw_body) |
User-Agent | netts-webhook/1.0 |
验证签名
签名采用 Stripe 规范方案(timestamp.body),基于我们发送的原始字节计算得出。请使用您的 secret 重新计算并进行恒定时间比对,若 X-Netts-Timestamp 超出 ±5 分钟 窗口则予以拒绝(防重放攻击)。
请使用接收到请求的端点所对应的密钥进行验签:主端点和备用端点具有各自独立的密钥。如果您的两个地址都由同一个处理程序提供服务,请根据请求到达的目标 URL 来选取密钥。
import hmac, hashlib, time
def verify(raw_body: bytes, sig_header: str, ts_header: str, secret: str) -> bool:
# freshness (anti-replay)
if abs(time.time() - int(ts_header)) > 300:
return False
signed = f"{ts_header}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_header)
# Flask example:
# ok = verify(request.get_data(),
# request.headers["X-Netts-Signature"],
# request.headers["X-Netts-Timestamp"], SECRET)推送语义(重要 — 至少一次)
推送机制保证至少一次(at-least-once):响应丢失可能导致重试,因此您可能会收到相同的事件。鉴于业务操作(释放 USDT)涉及资金安全:
- 必须进行去重 — 基于
delivery_id(和/或order_id)幂等地处理每个事件;重复事件直接作为无操作(no-op)处理。 - 在执行任何涉及资金的操作前校验 HMAC — 在签名匹配且
X-Netts-Timestamp处于有效期之前,不要信任请求体内容。 - 仅在持久化存储事件后再返回 2xx — 否则我们会(正确地)触发重试。
返回 2xx 响应以确认接收;任何非 2xx 响应或超时均会触发重试。
尝试顺序:
- 重试优先发送到您的
primary端点。重试窗口取决于订单类型:5m订单重试约 1 分钟,所有其他类型重试约 10 分钟。 - 如果超过时间窗口且您注册了
backup端点,推送将转移至该备用端点,并重新开始重试计划 — 使用备用端点自身的密钥进行签名。 - 仅当备用端点也耗尽重试后,该次推送才会被标记为失败(dead)。
整个流程中使用相同的 delivery_id,因此在主端点失败随后在备用端点成功的消息,对您的去重逻辑而言仍然属于同一个事件。
错误代码参考
| 代码 | 描述 | HTTP 状态码 |
|---|---|---|
10000 | 成功 (created / ok / updated / rotated) | 200 / 201 |
- | 已删除 (无响应体) | 204 |
4000 | 无效 / 不安全的 Webhook URL (非 https、私有/回环地址、包含凭据、过长) | 400 |
-1 | 无效的 API 密钥 / IP 不在白名单中 | 401 |
-1 | 未找到端点 (或不属于您) | 404 |
4090 | 达到端点数量上限 (最多 2 个: primary, backup) | 409 |
4091 | 请求的角色已被占用 — 请使用 PATCH 互换或删除现有端点 | 409 |
4220 | 无内容可更新 (PATCH 请求体为空) | 422 |
5003 | 创建端点失败 (请重试) | 503 |
速率限制
按 每个 API 密钥 进行限制(请求头 X-API-KEY):
| 周期 | 限制 |
|---|---|
| 1 秒 | 5 次请求 |
| 1 分钟 | 150 次请求 |
超出速率限制 (429)
{ "message": "API rate limit exceeded" }注意事项
- 密钥仅显示一次 — 在创建和轮换时提供。使用
GET/LIST绝不会返回该密钥。如果不慎丢失,请通过轮换操作获取新密钥。 - 两个端点,非扇出式广播: 一个
primary和一个backup。每个确认的订单只生成一个 Webhook 并推送到主端点;备用端点仅在主端点重试耗尽后使用。 - 零停机时间更改 URL: 将新地址注册为
backup,验证通过后,使用PATCH将其变更为primary— 该角色互换操作是原子性的。 - 暂停:
PATCH … {"is_active": false}可停止推送且无需删除端点。 - 仅推送成功事件:
delegation.confirmed、bandwidth.delegated、activation.confirmed。没有失败事件 — 失败或超时的订单不会生成 Webhook。 - 未来可能会添加新的事件类型。 请根据
event字段进行路由,并忽略您尚不处理的类型 — 无需注册任何新端点即可接收它们。 - 哈希在推送前会在链上进行验证(参见顶部说明):Webhook 要么携带全部已入块的哈希,要么完全不发送。
- URL 安全性校验: 在注册和每次编辑时均会验证 URL 的 SSRF 安全性;推送端在发送时也会重新进行验证。