接入你的 App
结果回调
Webhook 缩短的是“任务完成”到“你的应用知道”之间的延迟。它只是投递优化:尝试三次后网关就不再投递。任务状态接口始终是唯一可信来源。
#提交任务时注册回调
在提交请求里传 webhook_url。再加上 webhook_secret,每次投递都会带签名——不加就完全没有签名。
submit.sh
curl https://www.aisplice.store/v1/videos \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: video-job-8842" \
-d '{
"model": "doubao-seedance-2.5",
"prompt": "A cinematic product scene",
"webhook_url": "https://your.app/hooks/ai-tasks",
"webhook_secret": "whsec_replace_me"
}'必须是公网可路由地址
localhost、127.0.0.1、内网段、链路本地地址和云元数据地址会在任务提交时就被拒绝——不是等到投递才失败。开发阶段请用内网穿透。
必须是 http 或 https,且提交时可解析
网关会解析主机名并检查解析出的每一个地址。解析不了的域名会直接导致提交失败。
投递是尽力而为
重试三次后网关就不再投递,只记录日志。任务状态接口始终是唯一可信来源。
localhost 会在任务提交时被拒绝
回调域名会在任务被受理之前完成解析和校验,因此内网或回环地址会让提交本身失败——而不是一小时后投递才失败。开发阶段请把 webhook_url 指向公网隧道,或者干脆不传、改用轮询。
#请求头与负载
| 请求头 | 何时发送 | 该怎么用 |
|---|---|---|
| X-Xiaoxiang-Signature | 仅当任务提交时带了 webhook_secret 才会有。 | 格式为 sha256=<原始请求体的 HMAC-SHA256 十六进制值>。请在解析前用常量时间比较来校验。 |
| X-Xiaoxiang-Event | 每一次投递。 | 取值为 task.completed 或 task.failed——直接按它分发,不要再从负载里反推。 |
| X-Xiaoxiang-Event-Id | 每一次投递。 | 同一事件重试时保持不变。存下来并丢弃重复投递——这就是你的去重键。 |
| 字段 | 类型 | 是否返回 | 说明 |
|---|---|---|---|
| event_id | string | 总是返回 | 与 X-Xiaoxiang-Event-Id 一致,用它去重。 |
| event | string | 总是返回 | "task.completed" 或 "task.failed"。 |
| task_id | string | 总是返回 | 提交请求返回的 id——你这边的关联键。 |
| status | string | 总是返回 | 终态状态值,使用视频那套词汇——"completed" 或 "failed",图片和音乐任务也是如此。 |
| model | string | 已知时返回 | 产出该任务的模型;未知时不返回。 |
| url | string | 成功时返回 | 任务成功时的结果 URL。更推荐用 /content 拿到稳定的平台链接。 |
| fail_reason | string | 失败时返回 | 仅在失败时出现。 |
| quota | integer | 总是返回 | 实际消耗的额度(内部单位)。对账请以用量日志为准,而不是这个字段。 |
| created_at | integer | 总是返回 | 任务提交时间,Unix 秒。 |
| finished_at | integer | 总是返回 | 进入终态的时间,Unix 秒。 |
delivery.json
{
"event_id": "task_8f21c...:task.completed",
"event": "task.completed",
"task_id": "8f21c0d2-...",
"status": "completed",
"model": "doubao-seedance-2.5",
"url": "https://cdn.example.com/renders/8f21c0d2.mp4",
"quota": 128000,
"created_at": 1789000000,
"finished_at": 1789000184
}#验证原始请求体
对收到的原始字节计算 HMAC-SHA256,且必须在 JSON 解析之前——重新序列化会改变字节、导致比对失败。请用常量时间比较,不一致就拒绝。
import crypto from "node:crypto";
// Express: express.raw({ type: "application/json" }) — not express.json().
export function handleTaskWebhook(req, res) {
const received = req.get("X-Xiaoxiang-Signature") ?? "";
const expected =
"sha256=" +
crypto.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(req.body) // Buffer of the raw body
.digest("hex");
const a = Buffer.from(received);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
// Deduplicate first, acknowledge fast, then do the work off-request.
if (!seen.add(req.get("X-Xiaoxiang-Event-Id"))) return res.status(200).end();
queue.push(event);
res.status(200).end();
}#投递与去重
15 秒内完成应答
这是投递超时时间。慢于此就算一次失败投递,所以请先入队、先返回 2xx。
按事件 ID 去重
X-Xiaoxiang-Event-Id 在重试之间保持不变。在改动业务状态之前先存下它并丢弃重复。
预期三次重试,之后就没有了
投递失败后会在 5 秒、30 秒、2 分钟各重试一次。之后网关只记日志——不会再有任何投递。
#保留对账轮询器
Webhook 为尽力投递
对数据库里仍未进入终态的任务跑一个低频对账任务——一次恰好落在投递窗口的发布就足以漏掉一条。查询状态接口、修复记录,把 webhook 纯粹当成延迟优化。
还是没解决?
把请求 ID 或报错信息发给我们,我们来查。