Webhooks

Webhook 会把订单状态变化推送到你的服务器,让你无需轮询。创建 API key 时你可以提供一个 callback_url;如果提供了,该 key 所创建订单的每次状态变化都会 POST 到该地址,并使用该 key 的 secret 签名。

Webhook 是可选的。如果不设置 callback_url,请继续轮询 GET /api/v1/energy/orders/:order_id。

开启 webhook

  1. 在门户中打开 API keys,创建(或重新签发)一个 key。
  2. 填写 Webhook URL —— 一个由你控制的 https:// 端点。该地址会被校验为可公开访问;内网、回环与链路本地地址会被拒绝。
  3. 该 key 创建的订单会投递到此地址。

回调地址按 API key 配置。创建订单的那个 key 决定该订单的通知发往何处。

你会收到什么

向你的 callback_url 发起的 POST:

content-type: application/json
user-agent: tron-energy-aggregator-webhook/1.0
x-signature: t=1727520000,v1=<lowercase-hex>

请求体:

{
  "event": "order.status_changed",
  "event_id": "ord_9c2b7f0a:COMPLETED",
  "created_at": 1727520000,
  "data": {
    "order_id": "ord_9c2b7f0a",
    "status": "COMPLETED",
    "product": "TRON_ENERGY",
    "order_type": "RENTAL",
    "amount": 64400,
    "duration_minutes": 60,
    "target_address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
    "customer_price": "2.093",
    "order_total": "2.093",
    "actual_consumed": "2.093",
    "currency": "TRX",
    "created_at": "2026-09-22T12:00:00.000Z",
    "completed_at": "2026-09-22T12:00:05.000Z",
    "failure_reason": null
  }
}
  • event 恒为 order.status_changed。
  • data 与 GET /api/v1/energy/orders/:order_id 返回的结构完全一致;data.status 为 PENDING、COMPLETED、FAILED、RECONCILIATION_REQUIRED 之一。
  • event_id 形如 ${order_id}:${status} —— 对同一状态变化的多次重试保持稳定。请按它去重。

校验投递

HMAC 密钥是你的 API key 的 sk_… secret(与你给请求签名用的同一串)。签名为:

v1 = hex(HMAC-SHA256(secret, `${t}.${rawBody}`))

其中 t 是 x-signature 的 t= 分量(unix 秒),rawBody 是收到的原始 JSON 字节(未解析)。校验步骤:

  1. 若 |now − t| > 300 秒则拒绝。
  2. 对 ${t}.${rawBody} 重新计算 HMAC,并与 v1 值做恒定时间比较。

Python 与 Go 仅使用标准库;Rust 示例需要 hmac、sha2 与 hex。

import hashlib, hmac, re, time

def verify(raw_body: bytes, x_signature: str, secret: str) -> bool:
    t_match = re.search(r"(?:^|,)t=(\d+)", x_signature)
    v_match = re.search(r"(?:^|,)v1=([0-9a-f]+)", x_signature)
    if not t_match or not v_match:
        return False
    t = int(t_match.group(1))
    if abs(int(time.time()) - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v_match.group(1))

务必先读取原始请求体再解析 JSON —— 签名覆盖的是精确字节,重新序列化解析后的对象可能改变它们。

投递语义

  • 至少一次(at-least-once)。 失败的投递(网络错误、429、5xx)会以指数退避重试,最多 5 次。
  • 重复处理。 重试使用相同的 event_id,因此请把处理函数按 event_id 做成幂等。
  • 不保证顺序。 最终的状态变化始终是权威状态 —— 以 data.status 为准,不要假设先后顺序。
  • 2xx 即成功。 任何 2xx 响应都表示确认接收。3xx、429 及其他非 2xx 按上述规则视为失败。
  • 重试耗尽 后投递被丢弃;轮询仍是读取订单状态的兜底方式。

如果你的端点宕机时间超过重试窗口,请轮询 GET /api/v1/energy/orders/:order_id 进行对账。

安全

  • 地址必须为 https:// 且可公开路由;绝不跟随重定向。
  • 只有创建订单的那个 API key 会收到它的 webhook。
  • 吊销 key 会立即停止该 key 的后续投递。