接收事件
Webhook 投遞同簽章驗證
UnifyPort 點樣經 HTTP POST 將事件投遞去你嘅接收位址、每次投遞帶嘅 X-Device-* 請求標頭,以及點樣驗證 HMAC-SHA256 簽章去確認載荷可信。仲會講埋我哋預期嘅回應、重試同冪等。
投遞請求標頭
X-Device-Event-Id同一已解析事件重試期間不變嘅關聯 id。HistorySync 可能跨批次重用,唔可以當歷史訊息全域唯一鍵。
X-Device-Delivery-Id投遞關聯 id;冇另行設定時會回退做事件 id,所以唔保證每次 HTTP 嘗試都唯一。
X-Device-Timestamp今次投遞簽章時嘅 RFC 3339 UTC 時間(例如 2026-06-08T12:34:56Z)。佢係簽章字串嘅一部分,亦可以用嚟拒絕過期投遞。
X-Device-Signature對 "<X-Device-Timestamp>" + "." + "<原始請求內文>" 計出嘅十六進位 HMAC-SHA256。淨係當端點設定咗 signing_secret 先會有;關閉簽章時唔會傳送。
Content-Type一律係 application/json。
驗證簽章
Node.js (Express)
import crypto from 'crypto';
import express from 'express';
const app = express();
const SECRET = process.env.WEBHOOK_SIGNING_SECRET;
// express.raw keeps the body as the exact bytes we signed — never re-serialize.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
let timestamp = req.get('X-Device-Timestamp');
if (!timestamp) {
timestamp = '';
}
let signature = req.get('X-Device-Signature');
if (!signature) {
signature = '';
}
const hmac = crypto.createHmac('sha256', SECRET);
hmac.update(timestamp + '.');
hmac.update(req.body); // raw Buffer
const expected = hmac.digest('hex');
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid) return res.status(401).end();
const event = JSON.parse(req.body.toString('utf8'));
// handle event.type ...
res.status(200).end(); // any 2xx acknowledges the delivery
});Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SIGNING_SECRET"].encode()
@app.post("/webhook")
def webhook():
timestamp = request.headers.get("X-Device-Timestamp", "")
signature = request.headers.get("X-Device-Signature", "")
body = request.get_data() # raw bytes, exactly as delivered
expected = hmac.new(
SECRET, timestamp.encode() + b"." + body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
# handle event["type"] ...
return "", 200備註
- 用任何 2xx 狀態確認。回應內文會被讀取再丟棄;非 2xx 當做投遞失敗。
- 連線錯誤同 408 / 429 / 5xx 會喺首次投遞後按 max_attempts 重試;預設 3 次重試,即最多 4 次 HTTP 請求。目前冇 backoff,亦唔讀 Retry-After。
- 投遞語意係至少一次。普通事件可以用 X-Device-Event-Id 抵禦同一事件重試;HistorySync 要按 provider + account_id + conversation.id + messages[].id 合併。
- 請對原始請求位元組做驗證,而且要喺任何 JSON 解析 / 重新序列化之前。重新編碼內文會改變位元組、令簽章對唔上。
- 拒絕 X-Device-Timestamp 同本地時鐘相差太遠嘅投遞嚟限制重放——時間戳已經被簽章覆蓋。
- 簽章係按端點設定:喺建立或者更新 webhook 端點時設定 signing_secret 就開啟;留空就關閉,亦唔再傳送 X-Device-Signature 標頭。
- 投遞唔保證順序——同一會話嘅事件可能亂序到達(已讀回執可能早過佢指向嘅訊息到)。請按 occurred_at 排序(事件 id 做並列時嘅次序鍵)先至套用狀態。
- UnifyPort 冇 REST 訊息歷史查詢,亦唔保證重放漏咗嘅事件;呼叫端仍然要持久化 Webhook。WhatsApp best-effort HistorySync 只供近期啟動/重連銜接,唔係完整封存。