aisplice

Build it into your app

Result callbacks

Webhooks cut the latency between a task finishing and your app knowing. They are a delivery optimization: three attempts, then the gateway stops trying. The status endpoint stays the source of truth.

#Register the callback at submit time

Pass webhook_url on the submit request. Add webhook_secret and every delivery is signed — without it, nothing is.

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

Must be publicly routable

localhost, 127.0.0.1, private ranges, link-local and cloud metadata addresses are rejected when the task is submitted — not at delivery time. Use a tunnel while developing.

http or https, resolvable at submit time

The hostname is resolved and every returned address is checked. A name that does not resolve fails the submit.

Delivery is best-effort

Three retries, then the gateway stops and only logs. The status endpoint stays the source of truth.

localhost is rejected when the task is submitted

The callback host is resolved and checked before the task is accepted, so a private or loopback address fails the submit itself — not the delivery an hour later. While developing, point webhook_url at a public tunnel, or leave it out and poll.

#Headers and payload

HeaderSent whenWhat to do with it
X-Xiaoxiang-SignatureOnly when the task was submitted with webhook_secret.sha256=<hex HMAC-SHA256 of the raw body>. Verify before parsing, with a constant-time compare.
X-Xiaoxiang-EventEvery delivery.task.completed or task.failed — route on this instead of re-deriving intent from the payload.
X-Xiaoxiang-Event-IdEvery delivery.Stable across retries of the same event. Store it and drop repeats — this is your deduplication key.
FieldTypePresentParam description
event_idstringAlwaysMatches X-Xiaoxiang-Event-Id. Deduplicate on it.
eventstringAlways"task.completed" or "task.failed".
task_idstringAlwaysThe id returned by the submit call — your join key.
statusstringAlwaysTerminal status in the video vocabulary — "completed" or "failed", even for image and music tasks.
modelstringWhen knownModel that produced the task. Omitted when unknown.
urlstringOn successResult URL when the task succeeded. Prefer /content for a stable platform link.
fail_reasonstringOn failurePresent on failure only.
quotaintegerAlwaysQuota actually consumed, in internal units. Reconcile against the usage log, not this field.
created_atintegerAlwaysUnix seconds, task submission.
finished_atintegerAlwaysUnix seconds, terminal transition.
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
}

#Verify the raw request body

Compute HMAC-SHA256 over the exact bytes you received, before any JSON parsing — re-serializing the payload changes the bytes and breaks the comparison. Compare in constant time and reject on mismatch.

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();
}

#Delivery and deduplication

Acknowledge within 15 seconds

That is the delivery timeout. Anything slower counts as a failed attempt, so queue the work and return 2xx first.

Deduplicate on the event id

X-Xiaoxiang-Event-Id is stable across retries. Store it and drop repeats before touching business state.

Expect three retries, then silence

Failed deliveries retry after 5s, 30s and 2min. After that the gateway only logs — nothing else will arrive.

#Keep a reconciliation poller

Webhook delivery is best-effort

Run a low-frequency job over tasks that are still non-terminal in your database — a deploy during a delivery window is enough to miss one. Query the status endpoint, repair the row, and treat the webhook purely as a latency win.

Still stuck?

Send us the request ID or the error message and we will look into it.