跳到正文

API 文档 · 任务

Webhook 与验签

事件格式、X-Signature 验签与投递重试。

创建任务时传入 callback_url,任务到达终态时平台会 POST 一个事件:task.succeeded 或 task.failed。事件体的 data 是完整的任务对象,与查询接口一致。

签名密钥

在控制台 · 账号设置生成 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();
});