XBAI API

Completion callback

Receive a webhook when a task reaches a terminal state.

When callBackUrl is present in the create request, xbaiapi sends one POST request after the task reaches success or fail.

Payload

{
  "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
  }
}

Request headers

HeaderValue
Content-Typeapplication/json
User-Agentxbaiapi-webhook/1.0
X-Xbaiapi-Task-IdSame as data.taskId, useful for deduplication
X-Xbaiapi-TimestampUnix time in seconds when this attempt was signed
X-Xbaiapi-Signaturev1= followed by the hex HMAC-SHA256 signature

Delivery rules

RuleBehavior
TimingSent only for terminal states, never for progress updates
SuccessAny HTTP 2xx response acknowledges delivery
RetryNetwork errors, timeouts (10 seconds), 408, 429 and 5xx responses are retried, 4 attempts total
No retryAny other 4xx response stops delivery and marks the callback as failed. Fix your endpoint, then resend it from the Tasks page in the console
SignatureEvery attempt is signed again with a fresh timestamp
DuplicatesRare duplicates are possible, deduplicate by taskId

expireTime is when the result file is deleted, 30 days after task creation. It is null for failed tasks. Download the file before then.

Return a 2xx response quickly and move slow processing to your own queue.

Verify signatures

Anyone who learns your callback URL can send it requests. Verify the signature on every callback before you act on it, so you know the request came from xbaiapi and the body was not changed.

Your signing secret is on the API keys page of the console, under Webhook signing secret. It starts with whsec_, and the whole string, prefix included, is the HMAC key. Each account has one secret, used for every task. Rotating it replaces the old secret immediately, so callbacks sent after that, retries included, carry signatures made with the new secret.

To verify a callback:

  1. Read the raw request body as bytes, before any JSON parsing. Parsing and re-serializing changes the bytes and breaks the signature.
  2. Reject the request if X-Xbaiapi-Timestamp is more than 5 minutes away from your current time. This stops an old request from being replayed later.
  3. Compute HMAC-SHA256 over {timestamp}.{raw body} with your secret and hex-encode it.
  4. Compare it with the value after v1= in X-Xbaiapi-Signature using a constant-time comparison.
  5. Deduplicate by taskId, so a replay inside the 5-minute window has no effect either.

Return a 4xx status for requests that fail verification. xbaiapi does not retry 4xx responses, so if you configured the wrong secret, update it and resend the affected callbacks from the console.

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 keeps the body as a Buffer instead of parsing it
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"));
  // Deduplicate by data.taskId, then hand off to your own queue
  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()  # raw bytes, before JSON parsing
    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"]
    # Deduplicate by data["taskId"], then hand off to your own queue
    return "", 204

For actions that move money or publish content, such as crediting a user or releasing a video, also call GET /api/v1/jobs/recordInfo with the taskId and act on the state and result it returns. The callback then only tells you when to check.

On this page