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 + "." + 原始请求体))。验签步骤:
- 从头中解析
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();
});