Python AI Video Generation API — GenRelay Integration Guide

Aug 20, 2026·9 min read

You have a Python app and you want to add AI video generation. The models exist—Veo 3.1, Grok Imagine, Gemini Omni Flash—but they each have different APIs, billing models, and async patterns. GenRelay provides a single unified endpoint for all of them.

This guide covers everything you need to ship a working integration: authentication, model selection, job submission, async polling, error handling, and cost estimation in Python.

What Python dependencies do I need?

GenRelay's video API is a standard REST API. No proprietary SDK package is required—requests works for synchronous use, httpx for async. Install both or choose one:

pip install requests httpx aiohttp

Store your API key as an environment variable:

export GENRELAY_API_KEY="gr-..."
import os

GENRELAY_API_KEY = os.environ["GENRELAY_API_KEY"]
BASE_URL = "https://api.genrelay.ai/v1"
HEADERS = {"Authorization": f"Bearer {GENRELAY_API_KEY}"}

Never hardcode the key in source files—it will end up in version control.

Which video models does GenRelay support and when should I use each?

As of August 2026, GenRelay exposes four video generation models with distinct billing structures:

Model Modes Billing 720p 1080p
Veo 3.1 Lite t2v, i2v, ref2v Per second $0.060/s $0.120/s
Grok Imagine 1.0 t2v Per second $0.010/s
Grok Imagine 1.5 i2v Per second $0.022/s
Gemini Omni Flash t2v Per generation $0.10 flat $0.15 flat

Decision guide:

  • Veo 3.1 Lite — highest output quality; best for content that users will see directly. The most versatile model: it handles text-to-video, image-to-video, and reference-video modes. Use it when visual fidelity justifies the cost.
  • Grok Imagine 1.0 — the lowest per-second cost for text-to-video. Good for prototyping, internal tooling, or high-volume pipelines where cost is the binding constraint.
  • Grok Imagine 1.5 — image-to-video at mid-range pricing. A cost-effective Veo 1.5 alternative when you need i2v but don't require Veo quality.
  • Gemini Omni Flash — flat per-generation billing makes cost predictable regardless of clip length. Ideal when you're generating fixed-length clips and want straightforward unit economics.

How do I submit a text-to-video job?

Video generation is asynchronous: you POST a job, receive a job ID, then poll the status endpoint until the video is ready.

import requests
import os

GENRELAY_API_KEY = os.environ["GENRELAY_API_KEY"]
BASE_URL = "https://api.genrelay.ai/v1"
HEADERS = {"Authorization": f"Bearer {GENRELAY_API_KEY}"}

def submit_text_to_video(
    prompt: str,
    model: str = "veo-3.1-lite",
    resolution: str = "720p",
    duration: int = 8,
) -> str:
    """Submit a text-to-video job. Returns the job ID."""
    payload = {
        "model": model,
        "prompt": prompt,
        "resolution": resolution,
        "duration": duration,
    }
    r = requests.post(f"{BASE_URL}/videos/generations", headers=HEADERS, json=payload)
    r.raise_for_status()
    return r.json()["id"]


job_id = submit_text_to_video(
    prompt="Timelapse of clouds moving over a mountain range at golden hour, cinematic, 4K detail",
    model="veo-3.1-lite",
    resolution="720p",
    duration=8,
)
print(f"Job submitted: {job_id}")

The duration parameter is in seconds. Typical range is 4–8 seconds depending on the model. The endpoint returns immediately; actual generation happens in the background.

How do I poll for a video result in Python?

Poll the job status endpoint with a loop that exits on completed or failed:

import time

def poll_video_job(
    job_id: str,
    poll_interval: int = 15,
    initial_sleep: int = 30,
    timeout: int = 600,
) -> dict:
    """
    Wait for a video generation job to complete.
    Returns the full result dict on success.
    Raises RuntimeError on job failure, TimeoutError if timeout exceeded.
    """
    time.sleep(initial_sleep)  # skip the first N seconds — job won't be ready yet
    deadline = time.time() + timeout
    while time.time() < deadline:
        r = requests.get(f"{BASE_URL}/videos/generations/{job_id}", headers=HEADERS)
        r.raise_for_status()
        data = r.json()
        status = data.get("status")
        if status == "completed":
            return data
        if status == "failed":
            raise RuntimeError(f"Job failed: {data.get('error', 'unknown')}")
        print(f"[{job_id[:8]}] status={status} — checking again in {poll_interval}s")
        time.sleep(poll_interval)
    raise TimeoutError(f"Job {job_id} did not complete within {timeout}s")


result = poll_video_job(job_id, poll_interval=15, initial_sleep=30)
video_url = result["data"]["url"]
print(f"Video ready: {video_url}")

Recommended poll intervals by model:
- Veo 3.1 Lite: 15–20 seconds (typical generation: 90–180 seconds)
- Grok Imagine 1.0/1.5: 10 seconds (usually faster)
- Gemini Omni Flash: 10 seconds

The initial_sleep avoids hammering the status endpoint in the first few seconds when the job is guaranteed to still be queued. Set it to 30s for Veo 3.1 and 15s for Grok/Omni Flash.

How do I call image-to-video (i2v) in Python?

Image-to-video accepts an additional image_url parameter. Both Veo 3.1 Lite and Grok Imagine 1.5 support this mode.

def submit_image_to_video(
    prompt: str,
    image_url: str,
    model: str = "veo-3.1-lite",
    resolution: str = "720p",
    duration: int = 8,
) -> str:
    """Submit an image-to-video job. Returns the job ID."""
    payload = {
        "model": model,
        "prompt": prompt,
        "image_url": image_url,
        "resolution": resolution,
        "duration": duration,
    }
    r = requests.post(f"{BASE_URL}/videos/generations", headers=HEADERS, json=payload)
    r.raise_for_status()
    return r.json()["id"]


# Example: animate a product photo with slow rotation
job_id = submit_image_to_video(
    prompt="Product slowly rotating on a turntable, studio lighting, smooth motion, no background change",
    image_url="https://your-cdn.example.com/product-shot.jpg",
    model="grok-imagine-1.5",
    resolution="720p",
    duration=6,
)
result = poll_video_job(job_id, poll_interval=10, initial_sleep=15)
print(result["data"]["url"])

The image_url must be publicly accessible. If your source images are behind authentication, upload them to a CDN or temporary object storage bucket before passing the URL. Signed URLs with a 30-minute expiry work well here.

How do I generate multiple videos in parallel?

For batched generation across multiple prompts, submit all jobs first, then poll concurrently with asyncio:

import asyncio
import httpx

async def submit_job_async(client: httpx.AsyncClient, prompt: str, model: str) -> str:
    r = await client.post(
        f"{BASE_URL}/videos/generations",
        headers=HEADERS,
        json={"model": model, "prompt": prompt, "resolution": "720p", "duration": 8},
    )
    r.raise_for_status()
    return r.json()["id"]

async def poll_job_async(client: httpx.AsyncClient, job_id: str, interval: int = 10) -> dict:
    await asyncio.sleep(20)
    for _ in range(60):  # max 60 polls
        r = await client.get(f"{BASE_URL}/videos/generations/{job_id}", headers=HEADERS)
        r.raise_for_status()
        data = r.json()
        if data["status"] == "completed":
            return data
        if data["status"] == "failed":
            raise RuntimeError(f"Job {job_id} failed")
        await asyncio.sleep(interval)
    raise TimeoutError(f"Job {job_id} timed out")

async def batch_generate(prompts: list[str], model: str = "grok-imagine-1.0") -> list[dict]:
    async with httpx.AsyncClient(timeout=30) as client:
        # Submit all jobs first
        job_ids = await asyncio.gather(*[
            submit_job_async(client, p, model) for p in prompts
        ])
        # Poll all jobs concurrently
        return await asyncio.gather(*[
            poll_job_async(client, jid) for jid in job_ids
        ])

prompts = [
    "A river flowing through an autumn forest, aerial view, golden light",
    "City street at night in rain, neon reflections, slow motion",
    "Ocean waves crashing on rocks at sunset, wide angle",
]
results = asyncio.run(batch_generate(prompts, model="grok-imagine-1.0"))
for r in results:
    print(r["data"]["url"])

Don't submit more concurrent jobs than your plan's rate limit allows. Check your quota in the GenRelay console before running large batches.

What does AI video generation cost in Python workloads?

A utility function for cost estimation before you run:

def estimate_video_cost(model: str, resolution: str, duration_seconds: int) -> float:
    """Estimate video generation cost in USD."""
    # Flat-rate models: cost doesn't depend on duration
    flat_rates = {
        ("omni-flash", "720p"):  0.10,
        ("omni-flash", "1080p"): 0.15,
    }
    # Per-second models
    per_second_rates = {
        ("veo-3.1-lite", "720p"):     0.060,
        ("veo-3.1-lite", "1080p"):    0.120,
        ("veo-3.1-lite", "4k"):       0.180,
        ("grok-imagine-1.0", "720p"): 0.010,
        ("grok-imagine-1.5", "720p"): 0.022,
    }
    key = (model, resolution)
    if key in flat_rates:
        return flat_rates[key]
    rate = per_second_rates.get(key)
    if rate is None:
        raise ValueError(f"Unknown model/resolution combination: {key}")
    return round(rate * duration_seconds, 4)

# Cost examples for an 8-second 720p clip
print(estimate_video_cost("grok-imagine-1.0", "720p", 8))   # $0.08
print(estimate_video_cost("omni-flash", "720p", 8))          # $0.10 (flat)
print(estimate_video_cost("grok-imagine-1.5", "720p", 8))   # $0.176
print(estimate_video_cost("veo-3.1-lite", "720p", 8))       # $0.48

100-clip batch cost comparison at 720p × 8 seconds:

Model Per clip 100 clips Notes
Grok Imagine 1.0 $0.080 $8.00 Lowest cost t2v
Gemini Omni Flash $0.100 $10.00 Flat rate; predictable
Grok Imagine 1.5 $0.176 $17.60 Mid-range i2v
Veo 3.1 Lite $0.480 $48.00 Highest quality

For draft generation and internal tooling, Grok 1.0 or Omni Flash keep costs low. For final output that users see—trailers, product demos, marketing clips—Veo 3.1 Lite's quality difference typically justifies the higher cost.

FAQ

Is there an official GenRelay Python package on PyPI?
As of August 2026, there is no dedicated genrelay package. The API follows OpenAI-compatible conventions, so the requests / httpx patterns above are the idiomatic integration path.

How should I handle HTTP errors and retries in production?
Treat 429 (rate limit) and 503 (capacity) as retriable with exponential backoff. Treat 400 (bad request) and 422 (validation error) as logic bugs—check your payload. Use tenacity or a simple retry loop; don't silently swallow errors.

Are generated video URLs permanent?
No. Generated video URLs are time-limited (typically 24–72 hours). Download the video to your own storage—S3, GCS, R2, or similar—before the URL expires. Build download-and-store logic into your pipeline, not as an afterthought.

What is the maximum video duration per generation?
This varies by model. Veo 3.1 Lite currently supports up to 8 seconds per generation. For longer videos, generate multiple segments and concatenate with FFmpeg or a video processing library. See the Veo model page for current limits.

Can I run multiple Veo 3.1 jobs in parallel?
Yes, but Veo 3.1 Lite is resource-intensive on the backend—very high concurrency may result in queuing delays. Start with 3–5 parallel jobs and monitor completion times before scaling further. Grok and Omni Flash handle concurrency better for bulk workloads.


For deeper coverage of the async polling pattern—including how to handle queued vs. running states and per-model timeout tuning—see Async video generation API: polling patterns and timeout handling. For a full pricing breakdown across all video models, see Veo 3 API pricing.

Related posts

Join our DiscordPython AI Video Generation API — GenRelay Integration Guide — GenRelay