Webhook 通知
建立任務時傳入 callback_url,ofox 就會用 webhook 主動推送結果,你不需要輪詢。任務進入終態時,ofox 會向該地址傳送 POST,請求體攜帶完整的任務物件(傳送失敗會重試——見下方「重試策略」章節)。
callback_url 必須是 HTTPS 並通過 SSRF 校驗(拒絕私有網路、迴路、雲端中繼資料地址)。非法地址會在建立時被拒,返回 400 invalid_callback_url——見 建立影片任務。
事件類型
每個任務推送一個事件,與它最終進入的終態一一對應(該值同樣出現在 data.status 中)。
| 事件 | 觸發時機 |
|---|---|
video.generation.completed | 任務成功 |
video.generation.failed | 任務失敗 |
video.generation.cancelled | 任務被取消 |
video.generation.expired | 任務過期 |
推送體
每次推送都是向你 callback_url 發起的 POST,攜帶以下 header:
POST https://your-app.example/webhooks/ofox-video
Content-Type: application/json
X-Ofox-Idempotency-Key: 9bcf3c60-7db2-4e1a-a1b2-c3d4e5f60718-completed
X-Ofox-Signature: <HMAC-SHA256, base64url>請求體是一個信封結構。其中 data 欄位與同一任務的 查詢任務 回應體完全一致:
{
"event": "video.generation.completed",
"id": "9bcf3c60-7db2-4e1a-a1b2-c3d4e5f60718",
"created_at": 1776211400,
"data": {
"id": "9bcf3c60-7db2-4e1a-a1b2-c3d4e5f60718",
"status": "completed",
"model": "google/veo-3.1",
"prompt": "A golden retriever running on the beach at sunset",
"unsigned_urls": ["https://upstream.example/out.mp4"],
"mirror_urls": ["https://cdn.ofox.ai/videos/vgen_xxx.mp4?sig=..."],
"usage": {
"video_seconds": 5,
"video_cost": "0.4000000000"
},
"created_at": 1776211362,
"updated_at": 1776211400
}
}| 欄位 | 類型 | 說明 |
|---|---|---|
event | string | 上表中的事件類型之一 |
id | string | 任務 id(UUID) |
created_at | integer | 事件時間戳,Unix 秒 |
data | object | 完整任務物件——與 GET /v1/videos/{id} 結構相同 |
冪等
同一事件可能被推送多次——重試可能在你的端點已處理首次推送之後再次觸發。請用 X-Ofox-Idempotency-Key header 去重,其值為 <id>-<status>(例如 9bcf3c60-7db2-4e1a-a1b2-c3d4e5f60718-completed),每個 key 至多處理一次。
返回任意 2xx 狀態碼即視為已接收成功。非 2xx 回應或逾時會被判定為推送失敗並觸發重試,因此請盡快返回 2xx,把耗時處理放到非同步執行。
簽名驗證
每次推送都帶 X-Ofox-Signature header:HMAC-SHA256(請求體原文, 你的 webhook secret),base64url 編碼。用你的 secret 重新計算並做恆定時間比較,驗證通過後再信任推送內容。
import crypto from 'node:crypto'
// X-Ofox-Signature = HMAC-SHA256(raw request body, your webhook secret),
// base64url-encoded.
function verifyOfoxSignature(rawBody, signatureHeader, secret) {
if (typeof signatureHeader !== 'string') return false
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('base64url')
const a = Buffer.from(expected)
const b = Buffer.from(signatureHeader)
// Constant-time compare; guard length first (timingSafeEqual throws on mismatch).
return a.length === b.length && crypto.timingSafeEqual(a, b)
}必須對收到的原始位元組計算 HMAC,在任何 JSON 解析或重新序列化之前——否則簽名對不上。Express 中用 express.raw({ type: 'application/json' }) 取得原始請求體。
重試策略
推送失敗(非 2xx 或逾時)會自動重試,採用帶上限的指數退避加抖動(capped exponential backoff + jitter)——最多重試 10 次,總窗口約 40 分鐘。第 n 次重試前的間隔為 min(5·2^(n-1), 600) 秒,再加上隨機抖動。
| 重試 | 間隔 | 累計 |
|---|---|---|
| 1 | 5s | 5s |
| 2 | 10s | 15s |
| 3 | 20s | 35s |
| 4 | 40s | 1m15s |
| 5 | 80s | 2m35s |
| 6 | 160s | 5m15s |
| 7 | 320s | 10m35s |
| 8 | 600s(封頂) | 20m35s |
| 9 | 600s | 30m35s |
| 10 | 600s | 40m35s |
兜底
重試全部耗盡後,ofox 停止推送。任務結果不會遺失——你隨時可以輪詢 GET /v1/videos/{id} 取得最終狀態(受該端點的限流約束)。