Skip to main content

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:

  1. Registered webhooks — account-level endpoints managed via /api/webhooks or the dashboard; they receive every matching event.
  2. 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:

EndpointPurpose
GET /api/webhooksList webhooks
POST /api/webhooksCreate
GET /api/webhooks/{id}Details
PUT /api/webhooks/{id}Update (url, events, active)
DELETE /api/webhooks/{id}Delete
GET /api/webhooks/{id}/deliveriesDelivery log (status, attempts, response codes)
POST /api/webhooks/{id}/testSend 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:

HeaderValue
X-Zvid-Eventrender.completed or render.failed
X-Zvid-Delivery-IdUnique id for this delivery
X-Zvid-TimestampUnix time (seconds) the delivery was signed
X-Zvid-Job-IdThe render job id
X-Zvid-Signaturesha256=<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.