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} 获取最终状态(受该端点的限流约束)。