接入你的 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_id | string | 總是回傳 | 與 X-Xiaoxiang-Event-Id 一致,用它去重。 |
| event | string | 總是回傳 | "task.completed" 或 "task.failed"。 |
| task_id | string | 總是回傳 | 提交請求回傳的 id——你這邊的關聯鍵。 |
| status | string | 總是回傳 | 終態狀態值,使用影片那套詞彙——"completed" 或 "failed",圖片和音樂任務也是如此。 |
| model | string | 已知時回傳 | 產出該任務的模型;未知時不回傳。 |
| url | string | 成功時回傳 | 任務成功時的結果 URL。更推薦用 /content 拿到穩定的平台連結。 |
| fail_reason | string | 失敗時回傳 | 僅在失敗時出現。 |
| quota | integer | 總是回傳 | 實際消耗的額度(內部單位)。對帳請以用量日誌為準,而不是這個欄位。 |
| created_at | integer | 總是回傳 | 任務提交時間,Unix 秒。 |
| finished_at | integer | 總是回傳 | 進入終態的時間,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 或錯誤訊息傳給我們,我們來查。