Webhook Support for Async AI Video Jobs — GenRelay API Guide
You've submitted a Veo 3.1 video job. Generation takes anywhere from 60 seconds to several minutes depending on duration and resolution. A blocking poll loop in your request handler ties up the thread for that entire window — and if you have ten concurrent users, you're blocking ten threads. What your architecture actually needs is a completion event: "job done, here's the URL." That's the webhook pattern, and this guide explains how to implement it against GenRelay's async video API.
Does GenRelay Support Native Webhooks for Video Jobs?
GenRelay's video generation API uses an async job pattern: you submit a job and receive a job ID, then query GET /v1/videos/generations/{job_id} until the status reaches succeeded or failed. As of August 2026, the endpoint does not accept a webhook_url parameter or fire outbound HTTP callbacks natively.
The practical implication: you implement the notification layer yourself. The two standard approaches are a background polling worker (fires an HTTP POST to your server when the job completes) and server-sent events (pushes status to a browser client). Both are covered below, with working Python code for each.
How Does the Async Job Pattern Work?
Video generation on GenRelay is a two-step operation: submit and poll.
Step 1 — Submit the job and capture the ID:
import requests
HEADERS = {"Authorization": "Bearer YOUR_KEY"}
job = requests.post(
"https://genrelay.ai/v1/videos/generations",
headers=HEADERS,
json={
"model": "veo-3.1-lite",
"prompt": "product reveal animation, dark marble surface, dramatic studio lighting",
"duration": 8,
"resolution": "1080p"
}
).json()
job_id = job["id"]
print(f"Job submitted: {job_id}")
Step 2 — Poll until completion:
import time
POLL_INTERVAL_S = 10
while True:
result = requests.get(
f"https://genrelay.ai/v1/videos/generations/{job_id}",
headers=HEADERS
).json()
status = result["status"]
if status == "succeeded":
video_url = result["output"]["url"]
print(f"Complete: {video_url}")
break
elif status == "failed":
raise RuntimeError(result.get("error", "Generation failed"))
else:
# status is "pending" or "processing"
print(f"Status: {status}, checking again in {POLL_INTERVAL_S}s")
time.sleep(POLL_INTERVAL_S)
Poll interval matters. Hitting the status endpoint every second wastes API quota and adds noise to your request logs. Ten seconds is a reasonable default for Veo 3.1 and Grok Imagine jobs. Omni Flash typically completes faster — 5-second polling fits better there.
For per-model expected completion times and a production-grade timeout strategy, see the async video generation polling guide.
How Do I Add Webhook-Style HTTP Callbacks?
If your application is structured to receive webhook events — an HTTP POST from a third-party service when an async operation completes — you can replicate that pattern with a background polling worker.
The approach: when you submit a video job, store the job ID alongside the callback URL your application wants notified. A background thread (or queue worker) polls GenRelay and fires the HTTP POST to your callback URL when the job finishes.
Minimal implementation using Python threading:
import requests, threading, time
def poll_and_notify(job_id: str, callback_url: str, api_key: str, poll_interval: int = 10):
headers = {"Authorization": f"Bearer {api_key}"}
max_wait = 600 # 10-minute timeout
elapsed = 0
while elapsed < max_wait:
result = requests.get(
f"https://genrelay.ai/v1/videos/generations/{job_id}",
headers=headers
).json()
status = result["status"]
if status == "succeeded":
requests.post(callback_url, json={
"job_id": job_id,
"status": "succeeded",
"url": result["output"]["url"]
})
return
elif status == "failed":
requests.post(callback_url, json={
"job_id": job_id,
"status": "failed",
"error": result.get("error")
})
return
time.sleep(poll_interval)
elapsed += poll_interval
# Timeout — notify as failed
requests.post(callback_url, json={
"job_id": job_id,
"status": "failed",
"error": "Generation timed out"
})
# Submit a job, then launch the background watcher
job_id = submit_video_job(prompt="...") # your submission code
callback_url = "https://yourapp.com/webhooks/video-complete"
t = threading.Thread(
target=poll_and_notify,
args=(job_id, callback_url, "YOUR_KEY"),
daemon=True
)
t.start()
The daemon thread runs in the background; your main thread returns immediately. Your /webhooks/video-complete endpoint receives a POST with job status when generation completes.
For production workloads with multiple concurrent jobs, move to a task queue instead of raw threads. Threads don't survive process restarts and don't give you visibility into in-flight jobs. A Celery + Redis setup handles the same logic with retry support, persistence across restarts, and horizontal scaling:
# tasks.py — Celery task
from celery import Celery
import requests, time
app = Celery("video_jobs", broker="redis://localhost:6379/0")
@app.task(bind=True, max_retries=3)
def watch_video_job(self, job_id: str, callback_url: str, api_key: str):
headers = {"Authorization": f"Bearer {api_key}"}
result = requests.get(
f"https://genrelay.ai/v1/videos/generations/{job_id}",
headers=headers
).json()
status = result["status"]
if status in ("pending", "processing"):
# Re-queue the task with a 10-second countdown
raise self.retry(countdown=10)
# Terminal state — fire the callback
requests.post(callback_url, json={
"job_id": job_id,
"status": status,
"url": result.get("output", {}).get("url"),
"error": result.get("error")
})
Dispatch it with watch_video_job.delay(job_id, callback_url, api_key) immediately after job submission.
How Do I Stream Progress to a Browser?
For user-facing apps — a progress indicator while the user waits — server-sent events (SSE) let your backend push status updates to the browser without the client repeatedly polling your own server.
from flask import Flask, Response
import requests, time, json
app = Flask(__name__)
@app.route("/video-stream/<job_id>")
def stream_video_status(job_id):
api_key = "YOUR_KEY"
headers = {"Authorization": f"Bearer {api_key}"}
def generate():
while True:
result = requests.get(
f"https://genrelay.ai/v1/videos/generations/{job_id}",
headers=headers
).json()
status = result["status"]
payload = {"status": status}
if status == "succeeded":
payload["url"] = result["output"]["url"]
yield f"data: {json.dumps(payload)}\n\n"
return
elif status == "failed":
payload["error"] = result.get("error")
yield f"data: {json.dumps(payload)}\n\n"
return
else:
yield f"data: {json.dumps(payload)}\n\n"
time.sleep(8)
return Response(generate(), mimetype="text/event-stream")
In the browser, one EventSource connection replaces any client-side polling:
const source = new EventSource(`/video-stream/${jobId}`);
source.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.status === "succeeded") {
document.getElementById("video").src = data.url;
source.close();
} else if (data.status === "failed") {
console.error("Generation failed:", data.error);
source.close();
} else {
showSpinner(data.status); // "pending" or "processing"
}
};
Which Pattern Fits Your Use Case?
| Use case | Recommended pattern |
|---|---|
| Backend-to-backend notification | Background polling worker + HTTP POST callback |
| User-facing progress bar | Server-sent events (SSE) |
| CLI script or one-shot job | Inline blocking poll loop |
| 50+ concurrent production jobs | Celery / BullMQ task queue |
| Mobile app | Backend SSE or webhook; not direct device polling |
For a complete integration example including error handling, retry logic, and downloading to S3 after generation, see How to integrate AI video generation into your app.
Frequently Asked Questions
Does GenRelay accept a webhook_url field in the video request body?
As of August 2026, the video generation endpoint does not accept a webhook URL parameter. Use the background polling worker pattern described above to build event-driven behavior on top of the polling API.
How long are completed video output URLs valid?
GenRelay returns an output URL when the job succeeds. Download and store the file to your own storage (S3, GCS, R2) immediately after retrieval if you need long-term access.
What poll interval should I use for Omni Flash?
Omni Flash is billed per generation (not per second) and typically completes in under 30 seconds. A 5-second poll interval balances responsiveness against request overhead. For Veo 3.1 at 1080p, 8–10 seconds is a better starting point.
What happens if I poll a succeeded job again after retrieving the URL?
Polling a terminal-state job (succeeded or failed) returns the same result object each time. It's idempotent — safe to retry if your callback fires but the downstream write fails.
Can I check all my in-flight jobs in one call?
The GenRelay video API exposes per-job status via GET /v1/videos/generations/{job_id}. There is no bulk status endpoint; your polling worker tracks job IDs in its own queue. For Veo 3.1 jobs specifically, the job object includes model and resolution metadata to help correlate jobs with your application's records.