Guides
Webhooks
Get called when a video is ready.
Instead of polling, let Qulture call your server when a video (or image) is done.
Adding a webhook
Add webhook_url to the create request. It must be a public https:// URL. Works on POST /jobs, /images, /images/:id/animate and on regenerate / extend / upgrade.
curl -X POST https://qulture-webapi-staging-381114038302.us-central1.run.app/api/v1/studio/jobs \
-H "Authorization: Bearer $QULTURE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "text_to_video",
"prompt": "A neon-lit street market at night, slow camera push in",
"tier": "draft",
"duration": 5,
"aspect": "9:16",
"webhook_url": "https://your.app/hooks/qulture"
}'Events
| event | When |
|---|---|
video.succeeded | The video is ready (status ready). |
video.failed | The video failed or was blocked (status failed or blocked). |
image.succeeded | The image is ready. |
image.failed | The image failed or was blocked. |
ping | Sent by “Send test event” next to a key in the Console. |
The body is JSON. data is the same object GET /jobs/:id returns:
Qulture-Signature: t=1793232000,v1=5f2b9c…
Qulture-Event: video.succeeded
Qulture-Delivery: 0d3c8a6e-…
{
"id": "0d3c8a6e-…",
"event": "video.succeeded",
"created_at": "2026-10-29T00:21:12Z",
"data": {
"id": "8d67563e-…",
"status": "ready",
"video_url": "https://…/marked.mp4?…",
"download_url": "https://…",
"thumbnail_url": "https://…",
"duration_s": 5,
…
}
}Verifying signatures
Each API key has its own signing secret (whsec_…), shown when you create the key and any time in the Console. The Qulture-Signature header is t=<unix time>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <t>.<raw body> with your secret. Check it, and reject timestamps older than 5 minutes:
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.QULTURE_WEBHOOK_SECRET; // whsec_…
// Keep the raw body: the signature is over the exact bytes.
app.post("/hooks/qulture", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Qulture-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${parts.t}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const ok = fresh && parts.v1 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
if (!ok) return res.status(400).send("bad signature");
const event = JSON.parse(req.body);
if (event.event === "video.succeeded") {
console.log("Video ready:", event.data.video_url);
}
res.sendStatus(200);
});
app.listen(3000);Retries
Reply with any 2xx within 15 seconds. Anything else (or a timeout) is retried after 10 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h and 6 h. Use id (also Qulture-Delivery) to ignore duplicates. Redirects are not followed. If you revoke the key, its pending webhooks stop.
Testing locally
Use a tunnel such as ngrok http 3000 or cloudflared tunnel to get a public https URL for your laptop, then press Send test event next to your key in the Console.