Webhooks
Have ViddyFlow push job updates to your own endpoint instead of polling for them.
Setting one up
Set webhook_url when creating a job and ViddyFlow posts to it as the job moves along. Delivery is retried up to 3 times with exponential backoff, so a brief outage on your side will not lose the callback.
Every delivery is a JSON body of the shape { "event", "payload" }.
Events
| Event | Sent when |
|---|---|
job.running | A worker picked the job up and started processing. |
job.completed | Everything rendered. Carries the full deliverables list. |
job.failed | The job failed or was cancelled. Credits are refunded automatically. |
Completed payload
The deliverables list is identical to the one from GET /jobs/{id}/results, so most integrations never need to read manifest or artifacts at all.
{
"event": "job.completed",
"payload": {
"job_id": "abc123-def456-ghi789",
"deliverables": [
{
"type": "reel",
"title": "Balanced highlight reel",
"duration_seconds": 123.03,
"run_name": "balanced",
"size_bytes": 45234567,
"url": "https://artifacts.viddyflow.com/...&X-Amz-Signature=..."
},
{
"type": "short",
"index": 1,
"title": "Epic Clutch Play",
"duration_seconds": 38.4,
"tags": ["clutch", "gaming"],
"url": "https://artifacts.viddyflow.com/...&X-Amz-Signature=..."
}
],
"zip_url": "https://api.viddyflow.com/jobs/abc123-def456-ghi789/download-all?token=...",
"links_expire_at": "2026-08-24T15:25:44Z",
"manifest": {
"summary": {
"total_videos": 1,
"generated_video_duration_seconds": 307.72,
"total_shorts": 5,
"source_vod_duration_seconds": 8060.0
},
"highlights": [
{
"story": "Epic Gameplay Moment",
"start": 7846.0,
"end": 7889.52,
"duration": 43.52,
"run_name": "balanced"
}
],
"shorts_count": 5
}
}
}Reading artifacts directly? Inside artifacts.assets, every group is an array, including compilation and metadata, because one job can run several profiles. Read assets.compilation[0].url, not assets.compilation.url. Prefer deliverables, which is already flat.
Failure payload
The error field carries a message written for a person, not a stack trace, so you can surface it directly.
{
"event": "job.failed",
"payload": {
"job_id": "abc123-def456-ghi789",
"error": "This VOD is unavailable on Twitch. It may have been deleted, set to private, or restricted in your region.",
"refunded_credits": 138
}
}Verifying the signature
Reveal your webhook signing secret in API Access. Each delivery carries an X-ViddyFlow-Signature header: the HMAC-SHA256 hex digest of the raw request body, keyed with that secret.
Sign the raw bytes. Compare against the body exactly as received, before any JSON parsing or re-serialisation. Re-encoding the payload can change the bytes and break the comparison.
import hashlib
import hmac
def is_valid(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Flask example
@app.post("/viddyflow")
def receive():
if not is_valid(
request.get_data(),
request.headers.get("X-ViddyFlow-Signature", ""),
WEBHOOK_SECRET,
):
return "", 401
event = request.get_json()
if event["event"] == "job.completed":
for item in event["payload"]["deliverables"]:
print(item["type"], item["title"], item["url"])
return "", 200import crypto from "node:crypto";
// Express: keep the raw body so the signature can be checked against it.
app.post(
"/viddyflow",
express.raw({ type: "application/json" }),
(req, res) => {
const expected = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const signature = req.get("X-ViddyFlow-Signature") ?? "";
const ok =
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok) return res.sendStatus(401);
const { event, payload } = JSON.parse(req.body.toString("utf8"));
if (event === "job.completed") {
for (const item of payload.deliverables) {
console.log(item.type, item.title, item.url);
}
}
res.sendStatus(200);
},
);A note on links
Download URLs in a webhook payload are time-limited, and the underlying files are deleted about 30 days after the job last changed. If you are archiving output, pull the files down when the callback arrives rather than storing the URLs. See Your results for the full retention rules.