Skip to Content

Webhooks

Pass a callback_url when you create a task and ofox delivers the result by webhook instead of you polling for it. When the task reaches a terminal state, ofox sends a POST to your URL with the full task object in the body (a failed delivery is retried — see Retries).

callback_url must be HTTPS and pass SSRF validation (private, loopback, and cloud-metadata addresses are rejected). An invalid URL is rejected at creation time with 400 invalid_callback_url — see Create video.

Events

One event is delivered per task, matching the terminal status it settled into (the same value appears in data.status).

EventTriggered when
video.generation.completedThe task succeeded
video.generation.failedThe task failed
video.generation.cancelledThe task was cancelled
video.generation.expiredThe task expired

Delivery payload

Each delivery is a POST to your callback_url with these headers:

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>

The body is an envelope. Its data field is identical to the Get video status response body for the same task:

{ "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 } }
FieldTypeDescription
eventstringOne of the event types above
idstringTask id (UUID)
created_atintegerEvent timestamp, Unix seconds
dataobjectThe full task object — same shape as GET /v1/videos/{id}

Idempotency

The same event may be delivered more than once — a retry can fire after your endpoint already processed the first delivery. Deduplicate on the X-Ofox-Idempotency-Key header, which is <id>-<status> (for example 9bcf3c60-7db2-4e1a-a1b2-c3d4e5f60718-completed), and process each key at most once.

Respond with any 2xx status to acknowledge receipt. A non-2xx response or a timeout is treated as a failed delivery and triggers a retry, so return 2xx promptly and do any heavy work asynchronously.

Signature verification

Every delivery carries an X-Ofox-Signature header: HMAC-SHA256(raw request body, your webhook secret), base64url-encoded. Recompute it with your secret and compare in constant time before trusting the payload.

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) }

Compute the HMAC over the raw bytes exactly as received, before any JSON parse or re-serialization — otherwise the signature will not match. In Express, capture the body with express.raw({ type: 'application/json' }).

Retries

A failed delivery (non-2xx or timeout) is retried automatically with capped exponential backoff plus jitter — up to 10 retries over a window of about 40 minutes. The delay before retry n is min(5·2^(n-1), 600) seconds, plus random jitter.

RetryDelayCumulative
15s5s
210s15s
320s35s
440s1m15s
580s2m35s
6160s5m15s
7320s10m35s
8600s (capped)20m35s
9600s30m35s
10600s40m35s

Fallback

If all retries are exhausted, ofox stops attempting delivery. The task result is never lost — you can always poll GET /v1/videos/{id} to fetch the final state (subject to that endpoint’s rate limit).

Last updated on