Skip to Content

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 } }
字段类型说明
eventstring上表中的事件类型之一
idstring任务 id(UUID)
created_atinteger事件时间戳,Unix 秒
dataobject完整任务对象——与 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 重新计算并做恒定时间比较,验证通过后再信任推送内容。

verify-webhook.js
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) 秒,再加上随机抖动。

重试间隔累计
15s5s
210s15s
320s35s
440s1m15s
580s2m35s
6160s5m15s
7320s10m35s
8600s(封顶)20m35s
9600s30m35s
10600s40m35s

兜底

重试全部耗尽后,ofox 停止推送。任务结果不会丢失——你随时可以轮询 GET /v1/videos/{id} 获取最终状态(受该端点的限流约束)。

Last updated on