XBAI API

任务完成回调

在任务进入终态时接收 Webhook 通知。

创建任务时填写 callBackUrl 后,任务进入 success 或 fail 状态时,xbaiapi 会发送一次 POST 请求。

回调内容

{
  "code": 200,
  "msg": "success",
  "data": {
    "taskId": "xb_a1b2c3...",
    "model": "seedance-2.0",
    "state": "success",
    "resultUrl": "https://media.xbaiapi.com/video/2026/09/17/xb_a1b2c3.mp4",
    "failMsg": null,
    "creditsCharged": 165,
    "completeTime": 1789651045,
    "expireTime": 1792243000
  }
}

请求头

请求头取值
Content-Typeapplication/json
User-Agentxbaiapi-webhook/1.0
X-Xbaiapi-Task-Id与 data.taskId 相同,便于去重
X-Xbaiapi-Timestamp本次投递签名时的 Unix 时间戳(秒)
X-Xbaiapi-Signaturev1= 加上 HMAC-SHA256 签名的十六进制串

投递规则

规则行为
时机只在任务进入终态时发送,不推送中间进度
成功任意 HTTP 2xx 响应都表示接收成功
重试网络错误、超时(10 秒)以及 408、429、5xx 响应会触发重试,总共尝试 4 次
不重试其他 4xx 响应会停止投递,回调标记为失败。修好接收端后,可在控制台的任务页手动重发
签名每次投递都用新的时间戳重新签名
重复极端情况下可能重复投递,请按 taskId 去重

expireTime 是结果文件的删除时间,为任务创建后 30 天;失败任务为 null。请在此之前下载文件。

请尽快返回 2xx,耗时处理应转入你自己的任务队列。

验证签名

任何知道你回调地址的人都能向它发请求。处理回调之前请先校验签名,确认请求来自 xbaiapi,且内容没有被改过。

签名密钥在控制台的 API Key 页,回调签名密钥 一栏。密钥以 whsec_ 开头,包括前缀在内的整串都是 HMAC 的 key。每个账号一把密钥,所有任务共用。重置后旧密钥立即失效,此后发出的回调(包括重试)都用新密钥签名。

校验步骤:

  1. 按字节读取原始请求体,不要先做 JSON 解析。解析后再序列化,字节会变,签名就对不上了。
  2. 如果 X-Xbaiapi-Timestamp 与你的当前时间相差超过 5 分钟,拒绝请求,防止旧请求被拿来重放。
  3. 用密钥对 {时间戳}.{原始请求体} 计算 HMAC-SHA256,转成十六进制。
  4. 用常量时间比较,与 X-Xbaiapi-Signature 中 v1= 之后的值对比。
  5. 按 taskId 去重,这样 5 分钟窗口内的重放也不会产生影响。

校验不通过的请求请返回 4xx。xbaiapi 不会重试 4xx 响应,所以如果密钥配错了,更新密钥后在控制台重发受影响的回调即可。

Node.js(Express)
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.XBAIAPI_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 5 * 60;

function verifyXbaiapiSignature(rawBody, timestamp, signature) {
  if (!timestamp || !signature?.startsWith("v1=")) return false;
  const sentAt = Number(timestamp);
  const now = Math.floor(Date.now() / 1000);
  if (!Number.isInteger(sentAt) || Math.abs(now - sentAt) > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature.slice(3), "hex");
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

// express.raw 让请求体保持为 Buffer,不做解析
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyXbaiapiSignature(
    req.body,
    req.get("X-Xbaiapi-Timestamp"),
    req.get("X-Xbaiapi-Signature"),
  );
  if (!ok) return res.status(401).end();

  const { data } = JSON.parse(req.body.toString("utf8"));
  // 按 data.taskId 去重,再交给你自己的队列处理
  res.status(204).end();
});
Python(Flask)
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

SECRET = os.environ["XBAIAPI_WEBHOOK_SECRET"].encode()  # whsec_...
TOLERANCE_SECONDS = 5 * 60


def verify_xbaiapi_signature(raw_body: bytes, timestamp: str | None, signature: str | None) -> bool:
    if not timestamp or not signature or not signature.startswith("v1="):
        return False
    try:
        sent_at = int(timestamp)
    except ValueError:
        return False
    if abs(time.time() - sent_at) > TOLERANCE_SECONDS:
        return False

    expected = hmac.new(SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature[3:])


app = Flask(__name__)


@app.post("/webhook")
def webhook():
    raw_body = request.get_data()  # 原始字节,未经 JSON 解析
    if not verify_xbaiapi_signature(
        raw_body,
        request.headers.get("X-Xbaiapi-Timestamp"),
        request.headers.get("X-Xbaiapi-Signature"),
    ):
        abort(401)

    data = json.loads(raw_body)["data"]
    # 按 data["taskId"] 去重,再交给你自己的队列处理
    return "", 204

涉及资金或对外发布的操作,例如给用户加额度、上架视频,建议再用 taskId 调一次 GET /api/v1/jobs/recordInfo,以查询结果中的状态和产物为准。回调只用来通知你该去查了。

本页目录