Webhooks
작업 생성 시 callback_url을 전달하면, 사용자가 폴링하는 대신 ofox가 webhook으로 결과를 전달합니다. 작업이 terminal state에 도달하면 ofox는 전체 task object를 body에 담아 사용자의 URL로 POST를 보냅니다(전송이 실패하면 재시도됩니다. 아래 Retries 섹션을 참조하세요).
callback_url은 HTTPS여야 하며 SSRF 검증을 통과해야 합니다(private, loopback, cloud-metadata 주소는 거부됨). 유효하지 않은 URL은 생성 시점에 400 invalid_callback_url로 거부됩니다. 비디오 생성을 참조하세요.
Events
작업마다 하나의 event가 전달되며, 최종 terminal status와 일치합니다(같은 값이 data.status에 나타남).
| Event | 트리거 조건 |
|---|---|
video.generation.completed | 작업이 성공함 |
video.generation.failed | 작업이 실패함 |
video.generation.cancelled | 작업이 취소됨 |
video.generation.expired | 작업이 만료됨 |
Delivery payload
각 delivery는 다음 header와 함께 사용자의 callback_url로 전송되는 POST입니다:
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>Body는 envelope입니다. data 필드는 같은 작업에 대한 비디오 상태 조회 응답 body와 동일합니다:
{
"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 | 위 event type 중 하나 |
id | string | Task id(UUID) |
created_at | integer | 이벤트 timestamp, Unix seconds |
data | object | 전체 task object — GET /v1/videos/{id}와 같은 shape |
Idempotency
같은 event가 두 번 이상 전달될 수 있습니다. 사용자의 엔드포인트가 첫 delivery를 이미 처리한 뒤에도 retry가 실행될 수 있습니다. <id>-<status> 형식(예: 9bcf3c60-7db2-4e1a-a1b2-c3d4e5f60718-completed)인 X-Ofox-Idempotency-Key header를 기준으로 중복 제거하고, 각 key를 최대 한 번만 처리하세요.
수신 확인을 위해 아무 2xx status로 응답하세요. non-2xx 응답이나 timeout은 delivery 실패로 간주되어 retry를 트리거하므로, 즉시 2xx를 반환하고 무거운 작업은 비동기로 처리하세요.
Signature verification
모든 delivery에는 X-Ofox-Signature header가 포함됩니다: HMAC-SHA256(raw request body, your webhook secret), base64url-encoded. Payload를 신뢰하기 전에 secret으로 다시 계산하고 constant time으로 비교하세요.
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)
}JSON parse 또는 re-serialization 전에, 수신한 raw bytes 그대로 HMAC을 계산하세요. 그렇지 않으면 signature가 일치하지 않습니다. Express에서는 express.raw({ type: 'application/json' })로 body를 캡처하세요.
Retries
실패한 delivery(non-2xx 또는 timeout)는 capped exponential backoff와 jitter로 자동 재시도됩니다. 약 40분 동안 최대 10회 retry합니다. retry n 전 delay는 min(5·2^(n-1), 600)초에 random jitter를 더한 값입니다.
| Retry | Delay | 누적 |
|---|---|---|
| 1 | 5s | 5s |
| 2 | 10s | 15s |
| 3 | 20s | 35s |
| 4 | 40s | 1m15s |
| 5 | 80s | 2m35s |
| 6 | 160s | 5m15s |
| 7 | 320s | 10m35s |
| 8 | 600s (capped) | 20m35s |
| 9 | 600s | 30m35s |
| 10 | 600s | 40m35s |
Fallback
모든 retry가 소진되면 ofox는 delivery 시도를 중지합니다. 작업 결과는 손실되지 않습니다. 언제든지 GET /v1/videos/{id}를 폴링해 최종 상태를 가져올 수 있습니다(해당 엔드포인트의 rate limit 적용).