跳到主要內容

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 + "." + 原始請求內容))。驗證步驟:

  1. 從標頭解析出 t 與所有 v1:金鑰輪換後的 24 小時內會同時帶有新舊兩個 v1,任一相符即通過。
  2. 以未經解析的原始請求內容組合 t + "." + body,計算 HMAC-SHA256。
  3. 以常數時間比較與各個 v1 比對。
  4. 確認 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();
});