接入你的 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、私有網段、link-local 位址和雲端後設資料位址會在任務提交時就被拒絕——不是等到投遞才失敗。開發階段請使用通道(tunnel)服務。

必須是 http 或 https,且提交時可解析

閘道會解析主機名稱並檢查解析出的每一個位址。無法解析的網域會直接導致提交失敗。

投遞是盡力而為

重試三次後閘道就不再投遞,只記錄日誌。任務狀態端點始終是唯一可信來源。

localhost 會在任務提交時被拒絕

回呼主機會在任務被受理之前完成解析和檢查,因此私有或迴路位址會讓提交本身失敗——而不是一小時後投遞才失敗。開發階段請把 webhook_url 指向公開的通道(tunnel)服務,或者乾脆不傳、改用輪詢。

#請求標頭與負載

請求標頭何時傳送該怎麼用
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 或錯誤訊息傳給我們,我們來查。