API 文件 · 任務
Webhook 與簽章驗證
事件格式、X-Signature 簽章驗證與投遞重試。
建立任務時傳入 callback_url,任務進入終態時平台會 POST 一個事件:task.succeeded 或 task.failed。事件內容的 data 是完整的任務物件,與查詢 API 的回傳一致。
簽章金鑰
請在主控台 · 帳號設定產生 Webhook 簽章金鑰。金鑰只會顯示一次;輪換後新舊金鑰會並行 24 小時,方便你在不停機的情況下切換。
驗證簽章
產生簽章金鑰後,每個回呼請求都會帶有簽章標頭;未產生金鑰時回呼不帶簽章,正式環境請務必產生金鑰並驗證簽章:
X-Signature: t=<unix 秒>,v1=<hex>
其中 v1 = hex(HMAC_SHA256(secret, t + "." + 原始請求內容))。驗證步驟:
- 從標頭解析出
t與所有v1:金鑰輪換後的 24 小時內會同時帶有新舊兩個v1,任一相符即通過。 - 以未經解析的原始請求內容組合
t + "." + body,計算 HMAC-SHA256。 - 以常數時間比較與各個
v1比對。 - 確認
t與目前時間相差不超過 5 分鐘,以防止重送攻擊。
投遞與重試
- 回傳任何 2xx 即視為投遞成功;單次請求逾時為 10 秒,請盡快回應,耗時的處理請放入佇列非同步完成。重新導向(3xx)不會被跟隨,視為失敗。
- 首次投遞失敗(非 2xx 或逾時)後,會在 1 分鐘、5 分鐘、30 分鐘、2 小時、12 小時後重試,之後即放棄;任務結果不受影響,仍可查詢。
callback_url必須是可從公開網路存取的 http / https 網址。- 同一事件可能投遞多次,請依
id(evt_…)去除重複。 - 事件可能不依順序抵達,請以任務物件的狀態為準。
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.AITOKEN_WEBHOOK_SECRET;
// 必須使用原始請求內容驗證簽章,不能先 JSON.parse 再序列化
app.post("/hooks/video", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Signature") ?? ""; // t=1730259000,v1=5f2c...[,v1=舊金鑰簽章]
const pairs = header.split(",").map((kv) => kv.split("="));
const t = Number(pairs.find(([k]) => k === "t")?.[1]);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.status(400).end(); // 5 分鐘時間窗
const expected = Buffer.from(
crypto.createHmac("sha256", SECRET).update(`${t}.${req.body.toString("utf8")}`).digest("hex"),
);
// 輪換金鑰後 24 小時內會帶兩個 v1(新、舊),任一相符即通過
const ok = pairs
.filter(([k, v]) => k === "v1" && v?.length === expected.length)
.some(([, v]) => crypto.timingSafeEqual(Buffer.from(v), expected));
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body.toString("utf8"));
// 依 event.id 去重;盡快回傳 2xx,耗時處理放到佇列
res.status(204).end();
});