接入你的 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_idstring总是返回与 X-Xiaoxiang-Event-Id 一致,用它去重。
eventstring总是返回"task.completed" 或 "task.failed"。
task_idstring总是返回提交请求返回的 id——你这边的关联键。
statusstring总是返回终态状态值,使用视频那套词汇——"completed" 或 "failed",图片和音乐任务也是如此。
modelstring已知时返回产出该任务的模型;未知时不返回。
urlstring成功时返回任务成功时的结果 URL。更推荐用 /content 拿到稳定的平台链接。
fail_reasonstring失败时返回仅在失败时出现。
quotainteger总是返回实际消耗的额度(内部单位)。对账请以用量日志为准,而不是这个字段。
created_atinteger总是返回任务提交时间,Unix 秒。
finished_atinteger总是返回进入终态的时间,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 或报错信息发给我们,我们来查。