How to Integrate AI Video Generation into Your App — API Guide 2026
Your product needs a "generate video" button. Users describe a scene; your backend turns it into an MP4. The implementation challenge isn't the API call itself — it's the async lifecycle: video generation takes 30–120 seconds, you need to poll for completion, handle failures, and retrieve the output before the hosted URL expires.
This guide covers the full integration path using the GenRelay API, which provides access to Veo 3.1, Gemini Omni Flash, Grok Imagine 1.0, and Grok Imagine 1.5 through one endpoint. The patterns here apply to all four models.
How Do You Authenticate with the Video Generation API?
Authentication uses a Bearer token on every request. Set it as an environment variable and attach it via an Authorization header.
import os
import requests
API_KEY = os.environ["GENRELAY_API_KEY"]
BASE_URL = "https://api.genrelay.ai/v1"
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
})
Using a requests.Session lets you set auth once and reuse it across the job submission call and every polling request — relevant because you'll be making multiple requests per generation job.
Get your API key from the GenRelay console. Free credits are available on signup; paid plans start at $14/month with pay-as-you-go usage on top.
How Do You Submit a Video Generation Job?
Send a POST to /v1/videos/generations. The response is immediate and returns a job ID — not the video itself. The model processes the request asynchronously.
def submit_video_job(
model: str,
prompt: str,
duration: int = 5,
resolution: str = "720p",
) -> str:
"""Submit a video generation job. Returns the job ID string."""
response = session.post(
f"{BASE_URL}/videos/generations",
json={
"model": model,
"prompt": prompt,
"duration": duration,
"resolution": resolution,
},
timeout=30,
)
response.raise_for_status()
return response.json()["id"]
Core parameters:
| Parameter | Accepted values | Notes |
|---|---|---|
model |
veo-3-1-lite, omni-flash, grok-imagine-1-0, grok-imagine-1-5 |
Required |
prompt |
string, max 1000 chars | Describe scene, camera motion, lighting, style |
duration |
integer (seconds) | Veo 3.1 Lite: 5–8s; Grok: 4–6s; Omni Flash: fixed per billing tier |
resolution |
720p, 1080p |
Not all models support both — see model reference |
Writing prompts for video differs from image prompts. Include camera instructions: "slow dolly-in", "static wide shot", "tracking shot from left". Motion descriptions ("a candle flame flickering", "water dripping") produce more consistent results than scene descriptions alone.
How Do You Poll for Job Completion?
After submission, poll the status endpoint at regular intervals until the job reaches completed or failed. The polling interval matters: too short wastes requests; too long delays your user's experience.
import time
def poll_video_job(
job_id: str,
interval: int = 10,
timeout: int = 300,
) -> dict:
"""
Poll until the job completes or the timeout is reached.
Returns the completed job dict on success.
Raises TimeoutError or RuntimeError on failure.
"""
deadline = time.time() + timeout
while time.time() < deadline:
response = session.get(
f"{BASE_URL}/videos/generations/{job_id}",
timeout=15,
)
response.raise_for_status()
job = response.json()
status = job["status"]
if status == "completed":
return job
if status == "failed":
raise RuntimeError(
f"Job {job_id} failed: {job.get('error', 'unknown error')}"
)
# In-progress — wait and try again
time.sleep(interval)
raise TimeoutError(f"Job {job_id} did not complete within {timeout}s")
Recommended polling configuration by model:
| Model | Typical completion (5s clip) | Recommended interval | Timeout |
|---|---|---|---|
| Veo 3.1 Lite (720p) | 40–90s | 10s | 300s |
| Veo 3.1 Lite (1080p) | 60–120s | 15s | 360s |
| Grok Imagine 1.0 | 20–60s | 8s | 180s |
| Grok Imagine 1.5 | 25–70s | 10s | 240s |
| Gemini Omni Flash | 15–45s | 8s | 180s |
For production systems, consider exponential backoff on polling if you're handling high job volumes. See Async video generation API: how to poll for results for advanced patterns including backoff and concurrent job management.
How Do You Retrieve and Store the Output?
When the job status is completed, the response includes a url pointing to the generated video file. Download it promptly — hosted URLs expire after a period that varies by model (typically 1–24 hours).
import urllib.request
def download_video(job: dict, output_path: str) -> str:
"""Download the completed video to output_path. Returns the path."""
video_url = job["data"]["url"]
urllib.request.urlretrieve(video_url, output_path)
return output_path
Full flow, end to end:
job_id = submit_video_job(
model="veo-3-1-lite",
prompt="A barista pouring latte art in slow motion, close-up, warm café lighting",
duration=5,
resolution="720p",
)
print(f"Job submitted: {job_id}")
job = poll_video_job(job_id, interval=10, timeout=300)
path = download_video(job, f"/tmp/{job_id}.mp4")
print(f"Video saved: {path}")
In production, replace the download step with a direct upload to your object storage (S3, GCS, R2). Fetch the URL from the job response and stream it to your bucket without writing to local disk:
import boto3
def store_in_s3(job: dict, bucket: str, key: str) -> str:
video_url = job["data"]["url"]
s3 = boto3.client("s3")
with requests.get(video_url, stream=True, timeout=60) as r:
r.raise_for_status()
s3.upload_fileobj(r.raw, bucket, key)
return f"s3://{bucket}/{key}"
How Do You Handle Errors?
Three distinct error categories require different handling strategies.
401 — Invalid or expired key
try:
response.raise_for_status()
except requests.HTTPError as e:
if e.response.status_code == 401:
raise ValueError(
"API authentication failed — verify GENRELAY_API_KEY is set correctly"
)
raise
429 — Rate limited
Implement exponential backoff with jitter rather than a fixed retry:
import random
def submit_with_retry(model, prompt, duration, resolution, max_retries=3):
for attempt in range(max_retries):
try:
return submit_video_job(model, prompt, duration, resolution)
except requests.HTTPError as e:
if e.response.status_code == 429 and attempt < max_retries - 1:
wait = (2 ** attempt) + random.uniform(0, 1)
time.sleep(wait)
continue
raise
Job-level failure
The failed status in the polling loop includes an error field with a provider-level message. Log the full job response before raising — it contains enough context to debug prompt rejections, content policy flags, and model capacity issues.
Which Video Model Should You Use?
Select based on clip quality needs and cost tolerance. As of August 2026:
| Model | Pricing | Best suited for |
|---|---|---|
| Grok Imagine 1.0 | $0.010/s | Short clips, high-volume, budget-constrained |
| Grok Imagine 1.5 | $0.022/s | Better quality, supports image-to-video |
| Gemini Omni Flash | $0.10/gen (720p) · $0.15/gen (1080p) | Predictable per-generation cost |
| Veo 3.1 Lite | $0.060/s (720p) · $0.120/s (1080p) | High-quality text-to-video and image-to-video |
Example cost for 1,000 × 5-second clips at 720p:
- Grok 1.0: 1000 × (5 × $0.010) = $50
- Omni Flash: 1000 × $0.10 = $100
- Grok 1.5: 1000 × (5 × $0.022) = $110
- Veo 3.1 Lite: 1000 × (5 × $0.060) = $300
Grok 1.0 is the most cost-effective for high-volume consumer-facing features. Veo 3.1 Lite produces noticeably higher quality output for marketing or showcase content where visual fidelity matters. For a detailed comparison including 1080p and longer durations, see Veo 3 API pricing.
How Do You Add Image-to-Video Support?
Veo 3.1 and Grok Imagine 1.5 both support image-to-video generation. Pass an image_url alongside your prompt — the image anchors the first frame; the prompt describes the motion from that starting point.
def submit_image_to_video_job(
image_url: str,
prompt: str,
model: str = "veo-3-1-lite",
duration: int = 5,
) -> str:
response = session.post(
f"{BASE_URL}/videos/generations",
json={
"model": model,
"prompt": prompt,
"image_url": image_url,
"duration": duration,
"resolution": "720p",
},
timeout=30,
)
response.raise_for_status()
return response.json()["id"]
job_id = submit_image_to_video_job(
image_url="https://your-bucket.s3.amazonaws.com/product-shot.jpg",
prompt="The product slowly rotates, soft studio lighting, camera holds still",
model="veo-3-1-lite",
)
The image must be publicly accessible — GenRelay fetches it server-side. Use a signed URL with sufficient TTL if the image is in private storage.
FAQ
Does GenRelay support webhook callbacks instead of polling?
As of August 2026, GenRelay uses polling-based async for video jobs. The status endpoint is lightweight, so polling at 8–15 second intervals is efficient for most workloads. Webhook support is on the roadmap.
What video formats does the API return?
Output is MP4 (H.264) for all current models. Resolution and bitrate vary by model and resolution tier.
Can I generate videos longer than 8 seconds?
Veo 3.1 Lite supports up to 8 seconds per request. Grok Imagine models generate clips in the 4–6 second range. Longer sequences require generating and concatenating multiple clips.
What's the rate limit?
Rate limits depend on your plan tier. Check the response headers (X-RateLimit-Limit, X-RateLimit-Remaining) on each request. If you hit 429 responses, use the backoff pattern shown above. Contact support to request higher limits for high-volume workloads.
How do I pick the right prompt for consistent results?
Specificity beats abstraction. Instead of "a busy street", write "rush-hour pedestrians crossing an intersection, Tokyo, overcast sky, wide establishing shot." Include camera movement ("slow push-in", "handheld follow") and lighting ("golden hour", "fluorescent overhead") for more consistent output across Veo 3.1 and Grok models.
The integration pattern for AI video generation is: submit a job, poll the status endpoint, download the output on completion. GenRelay normalizes this lifecycle across Veo 3.1, Grok Imagine, and Omni Flash under one endpoint. The Python snippets above form a working foundation — add error handling, retry logic, and your storage upload step to make them production-ready.