Webhook 规格
TrackEasy 主动推送物流轨迹更新事件的协议规格。
为什么用 Webhook? 注册一次 URL,TrackEasy 自动监控所有你的运单,状态变化时主动推送。比反复轮询省 quota 省流量。
1. 注册 Webhook
POST /api/v1/webhooks
X-API-Key: tk_your_key
Content-Type: application/json
{
"url": "https://your-server.com/webhook/trackeasy",
"events": ["tracking_update", "tracking_delivered"]
}
响应中会返回 secret(whsec_xxx 格式)—— 仅此一次返回,请妥善保存用于验签。
{
"code": 200, "meta": 200, "message": "请求响应成功。",
"data": {
"url": "https://your-server.com/webhook/trackeasy",
"events": ["tracking_update", "tracking_delivered"],
"enabled": true,
"secret": "whsec_a1b2c3d4e5f6g7h8i9j0", // ⚠️ 仅创建时返回
"created_at": "2026-07-22T10:30:00",
"signature_header": "X-Webhook-Signature: t=<ts>,v1=<hex> (sha256(ts + \".\" + body))"
}
}
2. 事件类型
| 事件 | 触发时机 |
|---|---|
tracking_update | 轨迹有任何更新(新增事件、状态变化) |
tracking_delivered | 状态升级为「已签收」 |
tracking_exception | 状态变为「异常/无法投递/退回」 |
ping | 仅来自 /webhooks/test,用于测试 URL |
3. Payload 格式
{
"event": "tracking_update",
"created_at": "2026-07-22T10:30:45Z",
"data": {
"id": "f44cba625aa841eb7225f511",
"tracking_number": "9205512345600001234567",
"carrier": "usps",
"status": "派送中",
"events": [
{
"time": "2026-07-22T08:15:00Z",
"location": "ALBUQUERQUE, NM 87105",
"description": "Out for Delivery",
"status": "out_for_delivery"
},
{
"time": "2026-07-22T10:30:00Z",
"location": "ALBUQUERQUE, NM 87105",
"description": "Delivered, In/At Mailbox",
"status": "delivered"
}
]
}
}
4. HTTP Headers
| Header | 说明 |
|---|---|
Content-Type | 固定 application/json |
X-Webhook-Signature | 签名(Stripe 风格) |
X-Webhook-Timestamp | Unix 时间戳(秒) |
X-Webhook-Event | 事件类型(tracking_update 等) |
X-Webhook-Attempt | 第几次尝试(1 = 首次) |
User-Agent | TrackEasy-Webhook/1.0 |
5. 签名验证(必做)
每个 webhook 请求都有签名 header:
X-Webhook-Signature: t=1753178400,v1=a37084ab68ae16b77db1f8463f31be9fcc965e2515e03efecf8139bb1e511b06
X-Webhook-Timestamp: 1753178400
算法: HMAC-SHA256(secret, timestamp + "." + body)
⚠️ 防 replay:客户端必须检查
X-Webhook-Timestamp 与本地时间差 ≤ 5 分钟。超过视为重放攻击拒绝。
Python 验签示例
import hmac, hashlib, time
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_a1b2c3d4e5f6g7h8i9j0" # 注册时返回
@app.post("/webhook/trackeasy")
def handle():
sig_header = request.headers.get("X-Webhook-Signature", "")
ts = request.headers.get("X-Webhook-Timestamp", "")
body = request.get_data()
# 1. 防 replay (5 分钟 drift)
try:
ts_int = int(ts)
except ValueError:
abort(400, "bad timestamp")
if abs(time.time() - ts_int) > 300:
abort(400, "timestamp drift")
# 2. 验签
expected = hmac.new(
WEBHOOK_SECRET.encode(),
(ts + ".").encode() + body,
hashlib.sha256,
).hexdigest()
expected_header = f"t={ts},v1={expected}"
if not hmac.compare_digest(sig_header, expected_header):
abort(401, "bad signature")
# 3. 处理事件
event = request.json
print(f"📦 {event['event']}: {event['data']['tracking_number']} → {event['data']['status']}")
return "", 200 # 必须 2xx,否则会重试
Node.js 验签示例
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = 'whsec_a1b2c3d4e5f6g7h8i9j0';
app.post('/webhook/trackeasy', (req, res) => {
const sigHeader = req.headers['x-webhook-signature'] || '';
const ts = req.headers['x-webhook-timestamp'] || '';
const body = req.body;
// 1. 防 replay
if (Math.abs(Date.now()/1000 - parseInt(ts)) > 300) {
return res.status(400).send('drift');
}
// 2. 验签
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET)
.update(ts + '.' + body)
.digest('hex');
const expectedHeader = `t=${ts},v1=${expected}`;
if (!crypto.timingSafeEqual(Buffer.from(sigHeader), Buffer.from(expectedHeader))) {
return res.status(401).send('bad sig');
}
// 3. 处理
const event = JSON.parse(body);
console.log(`📦 ${event.event}: ${event.data.tracking_number}`);
res.sendStatus(200);
});
6. 重试机制
失败定义:HTTP 状态码不在 200-299,或超时(10 秒)。
最多重试 14 次,指数回退 2^n × 30s:
| 尝试 | 重试次数 | 延迟 (s) | 累计 (s) |
|---|---|---|---|
| 1 | 0 | 0 | 0 |
| 2 | 1 | 30 | 30 |
| 3 | 2 | 60 | 90 |
| 4 | 3 | 120 | 210 |
| 5 | 4 | 240 | 450 |
| 6 | 5 | 480 | 930 |
| 7 | 6 | 960 | 1890 |
| 8 | 7 | 1920 | 3810 |
| 9 | 8 | 3840 | 7650 |
| 10 | 9 | 7680 | 15330 |
| 11 | 10 | 15360 | 30690 |
| 12 | 11 | 30720 | 61410 |
| 13 | 12 | 61440 | 122850 |
| 14 | 13 | 122880 | 245730 |
14 次全失败后停止推送,不再重试。可调 GET /api/v1/webhooks/deliveries 看历史排查。
7. 安全要求
- 必须 HTTPS:生产环境禁止 HTTP(开发测试可用 HTTP)
- 禁私网 IP:注册时校验,禁止
127.0.0.1/10.x/192.168.x/172.16-31.x/ 内网域名 - 5 分钟 timestamp drift:必须做时钟检查
- 验签必做:未验签的 webhook 视为不安全
- 幂等处理:同一
id+created_at可能重投,客户端用(id, ts)去重
8. 测试
POST /api/v1/webhooks/test
X-API-Key: tk_your_key
响应:
{
"code": 200, "meta": 200, "message": "请求响应成功。",
"data": {
"reachable": true,
"http_status": 200,
"response_ms": 142
}
}
最佳实践: 生产环境先在测试环境跑通,再切到生产 webhook URL。
9. 投递历史
GET /api/v1/webhooks/deliveries
X-API-Key: tk_your_key
返回最近 50 条投递记录:
{
"code": 200, "meta": 200, "message": "请求响应成功。",
"data": {
"count": 12,
"items": [
{
"ts": "2026-07-22T10:30:45Z",
"event": "tracking_delivered",
"status_code": 200,
"attempt": 1,
"error": null
},
{
"ts": "2026-07-22T09:15:00Z",
"event": "tracking_update",
"status_code": 500,
"attempt": 3,
"error": "Internal Server Error"
}
]
}
}
调试 webhook 问题时先看这个列表 —— 显示每次投递的状态码 + 错误。