On this page
- Short answer
- Set up webhooks in five steps
- Should I use webhooks or polling for AI video jobs?
- How do I ask for a webhook?
- What does the webhook request contain?
- How do I verify the signature?
- How do I handle it in Express?
- How do I handle it in Python?
- What happens if my endpoint is down?
- What should the handler do with a completed job?
- How do I test webhooks locally?
- Frequently asked questions
Short answer
Add webhook_url to POST /v1/videos and KreatorFlow sends a signed POST when the job ends, with type job.completed or job.failed and the job in data.job. Verify X-KreatorFlow-Signature (HMAC-SHA256 of "timestamp.body" with your workspace secret), reply 2xx within 4 seconds and keep polling as a fallback.
Get started
Set up webhooks in five steps
- 1
Copy the signing secret
Open the developer console, go to Webhooks and reveal your workspace's signing secret. Store it on your server as KREATORFLOW_WEBHOOK_SECRET; rotate it there if it ever leaks.
- 2
Expose an HTTPS endpoint
Add a POST route such as /api/kreatorflow/webhook that reads the raw request body. Verification needs the exact bytes that were sent, so read the body as text before parsing JSON.
- 3
Send webhook_url with each job
Add webhook_url (callback_url also works) to POST /v1/videos, /v1/video-edits or /v1/images. Each job can point at a different URL, for example one per environment.
- 4
Verify, dedupe, answer fast
Check the signature and the timestamp, ignore an event id you have already handled, queue the work and reply 2xx. KreatorFlow waits 4 seconds per attempt.
- 5
Keep a polling fallback
Run a small sweep that polls GET /v1/jobs/{id} for jobs that have not reported back after a reasonable time. Webhooks are fast; the sweep makes sure nothing is ever missed.
Should I use webhooks or polling for AI video jobs?
Use both. A video takes minutes to render, so polling every few seconds wastes requests and holds workers open, while a webhook tells you the moment the job ends. Polling stays useful as a safety net: if your endpoint was down during every delivery attempt, a periodic GET /v1/jobs/{id} still picks the result up.
A common pattern: create the job with webhook_url, store its id with status processing, handle the webhook when it arrives, and once an hour poll any job still marked processing.
- Webhook: instant, no wasted requests, needs a public HTTPS endpoint
- Polling: works anywhere, costs a request every few seconds per job
- Both: webhook for speed, a slow sweep for anything missed
How do I ask for a webhook?
Add webhook_url to the create request. Nothing else changes: the response still returns the job id straight away, and you can still poll it.
POST /v1/videos body
{ "model": "kreator-video-seedance-2", "prompt": "Drone shot rising over a coastal town at golden hour", "duration": 5, "aspect_ratio": "16:9", "resolution": "720p", "webhook_url": "https://your-app.com/api/kreatorflow/webhook" }
What does the webhook request contain?
A JSON POST with these headers: X-KreatorFlow-Event-Id, X-KreatorFlow-Event-Type, X-KreatorFlow-Job-Id, X-KreatorFlow-Timestamp and X-KreatorFlow-Signature. The event type is job.<status> for the job's final status, such as job.completed or job.failed, and the event id is stable for that job and status, so it doubles as a deduplication key.
data.job carries the id, model, status, token costs, providerOutputUrl (the /v1/jobs/{id}/output link to download with your API key) and providerOutputExpiresAt, plus errorCode and errorMessage when the job did not complete. The prompt you sent is included too, so you can match the clip to your own records without a lookup.
Example delivery (trimmed)
POST /api/kreatorflow/webhook X-KreatorFlow-Event-Id: evt_3f1c0a9e..._completed X-KreatorFlow-Event-Type: job.completed X-KreatorFlow-Job-Id: 3f1c0a9e-... X-KreatorFlow-Timestamp: 1792252800 X-KreatorFlow-Signature: t=1792252800,v1=5d1f...e9 { "object": "kreatorflow.developer_webhook_event", "id": "evt_3f1c0a9e..._completed", "type": "job.completed", "created": "2026-10-17T12:00:00.000Z", "data": { "job": { "id": "3f1c0a9e-...", "mediaType": "video", "modelId": "kreator-video-seedance-2", "status": "completed", "finalTokenCost": 11, "providerOutputUrl": "https://kreatorflow.ai/v1/jobs/3f1c0a9e-.../output", "providerOutputExpiresAt": "2026-10-18T12:00:00.000Z", "errorCode": null, "errorMessage": null } } }
How do I verify the signature?
X-KreatorFlow-Signature looks like t=1792252800,v1=<hex>. Recompute HMAC-SHA256 over the string "<t>.<raw body>" with your signing secret, compare it to v1 in constant time, and reject the request if the timestamp is more than a few minutes old. That stops forged requests and replays of old ones.
Two mistakes break verification: parsing the JSON before hashing (re-serialising changes the bytes) and comparing with == (timing leaks). Both handlers below avoid them.
Next.js route handler (TypeScript)
// app/api/kreatorflow/webhook/route.ts (Next.js App Router) import { createHmac, timingSafeEqual } from "node:crypto"; const SECRET = process.env.KREATORFLOW_WEBHOOK_SECRET!; const TOLERANCE_SECONDS = 300; function verify(rawBody: string, header: string | null) { if (!header) return false; const parts = Object.fromEntries(header.split(",").map((part) => part.split("=") as [string, string])); const expected = createHmac("sha256", SECRET).update(`${parts.t}.${rawBody}`).digest("hex"); const given = parts.v1 ?? ""; const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < TOLERANCE_SECONDS; return fresh && given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected)); } export async function POST(request: Request) { const rawBody = await request.text(); if (!verify(rawBody, request.headers.get("x-kreatorflow-signature"))) { return new Response("bad signature", { status: 401 }); } const event = JSON.parse(rawBody); // Deduplicate on event.id, then queue the work and answer fast (the sender waits 4 seconds). await saveEventOnce(event.id, event.type, event.data.job); return new Response("ok"); }
How do I handle it in Express?
Use express.raw on this route so req.body is the untouched Buffer; a global express.json() would parse it first and the hash would no longer match.
Express (Node.js)
import express from "express"; import { createHmac, timingSafeEqual } from "node:crypto"; const app = express(); app.post("/kreatorflow/webhook", express.raw({ type: "application/json" }), (req, res) => { const raw = req.body.toString("utf8"); const parts = Object.fromEntries((req.get("X-KreatorFlow-Signature") ?? "").split(",").map((p) => p.split("="))); const expected = createHmac("sha256", process.env.KREATORFLOW_WEBHOOK_SECRET).update(`${parts.t}.${raw}`).digest("hex"); const ok = typeof parts.v1 === "string" && parts.v1.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) && Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; if (!ok) return res.status(401).send("bad signature"); const event = JSON.parse(raw); queueJob(event); // download the output and update your records outside the request res.sendStatus(200); });
How do I handle it in Python?
Flask's request.get_data gives the raw body; hmac.compare_digest does the constant-time comparison.
Flask (Python 3)
import hmac, hashlib, os, time from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ["KREATORFLOW_WEBHOOK_SECRET"].encode() @app.post("/kreatorflow/webhook") def kreatorflow_webhook(): raw = request.get_data(as_text=True) parts = dict(p.split("=", 1) for p in request.headers.get("X-KreatorFlow-Signature", "").split(",") if "=" in p) expected = hmac.new(SECRET, f'{parts.get("t", "")}.{raw}'.encode(), hashlib.sha256).hexdigest() fresh = abs(time.time() - int(parts.get("t", "0"))) < 300 if not (fresh and hmac.compare_digest(expected, parts.get("v1", ""))): abort(401) event = request.get_json() enqueue(event) # handle it in a worker; reply now return "ok", 200
What happens if my endpoint is down?
Each event is attempted up to 3 times in quick succession, with a 4-second timeout per attempt; any 2xx response counts as delivered. The attempts are close together, so a deploy or an outage of more than a few seconds can miss an event, which is exactly why the polling sweep matters.
Because a retry can arrive after you already processed the first attempt, make the handler idempotent: store each event id and skip ids you have seen.
What should the handler do with a completed job?
Answer first, work second. Put the event on a queue, reply 200, and let a worker download providerOutputUrl with your API key, save the file to your storage, and mark the job done in your database. For failed or blocked jobs, show the errorMessage to your user or adjust the input and create a new job.
The output link is a private download that needs your API key and expires (see providerOutputExpiresAt), so keep your own copy: write the file to your own bucket and serve it from your CDN. The storage guide shows the pattern.
How do I test webhooks locally?
Expose your local server with a tunnel (for example ngrok or Cloudflare Tunnel), use the tunnel URL as webhook_url and create a short, cheap job, such as a 4-second clip on the Fast model at 480p. Log the raw body and headers on the first run so you can check the signature code against a real delivery.
Frequently asked questions
Which events does the KreatorFlow API send?
One event when a job reaches its final status, typed job.<status>: job.completed, job.failed, job.canceled or job.blocked. data.job carries the job, including the output link when it completed.
Where is the webhook signing secret?
In the developer console under Webhooks. It is one secret per workspace; reveal it there and rotate it if it leaks.
How is the signature calculated?
HMAC-SHA256 with your signing secret over "<t>.<raw body>", where t is the Unix timestamp in the X-KreatorFlow-Signature header (t=...,v1=...). Compare your result to v1 in constant time.
How long does KreatorFlow wait for my endpoint?
4 seconds per attempt, 3 attempts in total. Reply 2xx quickly and do the slow work in a background job.
Can a webhook arrive twice?
Yes, if an attempt timed out after your server had already handled it. Store the event id (X-KreatorFlow-Event-Id) and ignore repeats.
Do I still need polling?
Keep a slow sweep as a fallback for jobs whose webhook never arrived, for example because your endpoint was down. GET /v1/jobs/{id} always has the current status.
Do webhooks cost extra?
No. Jobs are billed per second of video (or per image) whether you use webhooks or polling.



