Webhooks
Webhooks push render results to your server the moment a job finishes — no
polling. Zvid POSTs a JSON delivery to your endpoint on
render.completed and render.failed events, signs every delivery
with HMAC, and retries failures with exponential backoff.
There are two ways to receive them, usable together:
- Registered webhooks — account-level endpoints managed via
/api/webhooksor the dashboard; they receive every matching event. - Per-request
webhookUrl— a one-off URL passed on a render or bulk submission; it receives that job's completion only.
Registering a webhook
curl -X POST https://api.zvid.io/api/webhooks \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/zvid",
"events": ["render.completed", "render.failed"]
}'
The response includes the webhook's secret — store it; you'll use it to
verify signatures. All webhook endpoints accept JWT or x-api-key auth:
| Endpoint | Purpose |
|---|---|
GET /api/webhooks | List webhooks |
POST /api/webhooks | Create |
GET /api/webhooks/{id} | Details |
PUT /api/webhooks/{id} | Update (url, events, active) |
DELETE /api/webhooks/{id} | Delete |
GET /api/webhooks/{id}/deliveries | Delivery log (status, attempts, response codes) |
POST /api/webhooks/{id}/test | Send a test delivery |
Webhook URLs must be public https/http addresses — private hosts, local
addresses, and non-standard ports are rejected (SSRF protection). The number
of webhooks is plan-limited (Free: 1).
The delivery
Each delivery is a POST with these headers:
| Header | Value |
|---|---|
X-Zvid-Event | render.completed or render.failed |
X-Zvid-Delivery-Id | Unique id for this delivery |
X-Zvid-Timestamp | Unix time (seconds) the delivery was signed |
X-Zvid-Job-Id | The render job id |
X-Zvid-Signature | sha256=<hex HMAC> (registered webhooks with a secret) |
The body carries the same job object you'd get from GET /api/jobs/{id} —
state, result (with the video/image URL) or failedReason.
Verifying the signature
The signature is HMAC-SHA256(secret, "<timestamp>.<raw body>"), hex-encoded,
prefixed with sha256=:
const crypto = require("crypto");
function verifyZvidSignature(req, secret) {
const timestamp = req.headers["x-zvid-timestamp"];
const signature = req.headers["x-zvid-signature"];
const expected =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${req.rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Reject deliveries with stale timestamps (e.g. older than 5 minutes) to prevent replays. Use the raw request body — re-serialized JSON may not match.
Retries
A delivery is considered failed when your endpoint is unreachable or returns a non-2xx status. Zvid retries up to 5 attempts with exponential backoff (30 s, 60 s, 2 m, 4 m). Every attempt is visible in the deliveries log.
Per-request webhookUrl
Skip registration entirely by passing webhookUrl on the render submission:
curl -X POST https://api.zvid.io/api/render/api-key \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "payload": { ... }, "webhookUrl": "https://example.com/hooks/render-done" }'
Works on bulk submissions too. Per-request deliveries carry the same headers and body shape.