API v1 · Webhook 规格

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"]
}

响应中会返回 secretwhsec_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-TimestampUnix 时间戳(秒)
X-Webhook-Event事件类型(tracking_update 等)
X-Webhook-Attempt第几次尝试(1 = 首次)
User-AgentTrackEasy-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)
1000
213030
326090
43120210
54240450
65480930
769601890
8719203810
9838407650
109768015330
11101536030690
12113072061410
131261440122850
1413122880245730

14 次全失败后停止推送,不再重试。可调 GET /api/v1/webhooks/deliveries 看历史排查。

7. 安全要求

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 问题时先看这个列表 —— 显示每次投递的状态码 + 错误。