How to Build an Image-to-Video Pipeline with API | GenRelay
You have a product image — a sneaker, a piece of furniture, a car shot from a studio — and you want to animate it into a short video clip. Doing this directly against individual model APIs means separate credentials, different request shapes, and inconsistent async patterns for every provider. This tutorial walks through building a complete image-to-video (i2v) pipeline using GenRelay, a unified generative media API that routes to Veo 3.1, Grok Imagine 1.5, and other i2v models through one consistent endpoint.
By the end you'll have working Python code that submits an image, polls for completion, saves the output video, and knows exactly what each clip costs.
Which Models Support Image-to-Video on GenRelay?
As of August 2026, GenRelay offers two image-to-video models:
| Model | Max Duration | Resolution Options | Billing |
|---|---|---|---|
| Veo 3.1 (i2v mode) | 8 s | 720p, 1080p, 4K | per second × resolution tier |
| Grok Imagine 1.5 | 5 s | 720p | $0.022/s flat |
Veo 3.1 per-second rates at 720p: $0.060/s. A 5-second clip costs $0.30; 8 seconds costs $0.48.
Veo 3.1 at 1080p: $0.120/s. A 5-second clip costs $0.60.
Grok 1.5: $0.022/s regardless of resolution. A 5-second clip costs $0.11.
Choose Veo 3.1 when you need high resolution or longer clips. Choose Grok 1.5 when you're generating at volume and 720p output is sufficient.
How Do You Authenticate with the GenRelay API?
Every request requires a Bearer token in the Authorization header. Get your API key from the GenRelay dashboard.
import os
import requests
GENRELAY_API_KEY = os.environ["GENRELAY_API_KEY"]
BASE_URL = "https://genrelay.ai/v1"
headers = {
"Authorization": f"Bearer {GENRELAY_API_KEY}",
"Content-Type": "application/json",
}
Store the key in an environment variable — never hardcode it in source files. See the image API auth guide for key rotation practices.
How Do You Submit an Image-to-Video Job?
The i2v endpoint accepts a publicly accessible HTTPS image URL or a base64-encoded image string. Using a URL is simpler when your image is already in cloud storage.
def submit_i2v_job(
image_url: str,
prompt: str,
model: str = "veo-3.1",
duration: int = 5,
resolution: str = "720p",
) -> str:
"""Submit an i2v job and return the job ID."""
payload = {
"model": model, # "veo-3.1" or "grok-imagine-1.5"
"prompt": prompt, # describe the motion, not the subject
"image_url": image_url, # publicly accessible HTTPS URL
"duration": duration, # seconds: 1–8 for Veo 3.1, 1–5 for Grok 1.5
"resolution": resolution, # "720p" | "1080p" | "4k" (Veo 3.1 only)
}
r = requests.post(
f"{BASE_URL}/videos/generations",
headers=headers,
json=payload,
timeout=30,
)
r.raise_for_status()
return r.json()["id"]
job_id = submit_i2v_job(
image_url="https://cdn.example.com/products/sneaker-white.jpg",
prompt="The sneaker slowly rotates 360 degrees on a clean white studio surface, cinematic lighting",
model="veo-3.1",
duration=5,
)
print(f"Job submitted: {job_id}")
Key parameters explained:
- image_url: source image must be a public HTTPS URL (JPG or PNG, minimum 512×512 recommended)
- prompt: focus on motion and camera behavior — the model reads subject content from the image itself
- duration: integer seconds within model limits
- resolution: only Veo 3.1 supports 1080p and 4K; Grok 1.5 outputs 720p only
How Do You Poll for Completion?
Video generation is asynchronous. Typical turnaround for a 5-second clip is 30–90 seconds. Poll the job status endpoint until status is succeeded or failed.
import time
def poll_job(job_id: str, interval: int = 10, timeout: int = 300) -> dict:
"""Poll until the job completes or timeout is reached."""
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(
f"{BASE_URL}/videos/generations/{job_id}",
headers=headers,
timeout=15,
)
r.raise_for_status()
data = r.json()
status = data["status"]
if status == "succeeded":
return data
if status == "failed":
raise RuntimeError(f"Job {job_id} failed: {data.get('error')}")
# status is "processing" or "queued" — wait and retry
print(f" [{status}] waiting {interval}s…")
time.sleep(interval)
raise TimeoutError(f"Job {job_id} did not complete within {timeout}s")
result = poll_job(job_id)
video_url = result["output"]["url"]
print(f"Video ready: {video_url}")
For high-volume pipelines, see async video API polling for an asyncio-based implementation with exponential backoff and concurrent job management.
How Do You Download and Store the Output?
The output.url field is a pre-signed URL that expires after a few hours. Download the file immediately and copy it to your own storage.
import pathlib
def download_video(url: str, dest: str = "output.mp4") -> str:
"""Download the generated video to a local file."""
with requests.get(url, stream=True, timeout=60) as r:
r.raise_for_status()
pathlib.Path(dest).write_bytes(r.content)
return dest
local_path = download_video(video_url, dest="sneaker-360.mp4")
print(f"Saved to {local_path}")
In production, upload the file to S3 or GCS immediately rather than relying on the pre-signed URL for serving.
What Does This Pipeline Cost?
Cost depends on model, resolution, and clip duration:
| Clip | Model | Resolution | Duration | Cost |
|---|---|---|---|---|
| Product rotation | Veo 3.1 | 720p | 5 s | 5 × $0.060 = $0.30 |
| Product rotation | Veo 3.1 | 1080p | 5 s | 5 × $0.120 = $0.60 |
| Product rotation | Grok 1.5 | 720p | 5 s | 5 × $0.022 = $0.11 |
| Long loop | Veo 3.1 | 720p | 8 s | 8 × $0.060 = $0.48 |
For an e-commerce catalog generating one 5-second 720p clip per product:
| Catalog size | Veo 3.1 (720p) | Grok 1.5 (720p) |
|---|---|---|
| 100 products | $30 | $11 |
| 500 products | $150 | $55 |
| 2,000 products | $600 | $220 |
Grok 1.5 gives roughly a 3× cost reduction at the expense of motion quality. Test both on a sample batch before committing to one for a large catalog run.
Full Pipeline in One Function
import os, time, requests, pathlib
KEY = os.environ["GENRELAY_API_KEY"]
BASE = "https://genrelay.ai/v1"
H = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
def i2v_pipeline(
image_url: str,
prompt: str,
model: str = "veo-3.1",
duration: int = 5,
resolution: str = "720p",
dest: str = "output.mp4",
) -> str:
# 1. Submit job
r = requests.post(f"{BASE}/videos/generations", headers=H, json={
"model": model, "prompt": prompt,
"image_url": image_url, "duration": duration, "resolution": resolution,
}, timeout=30)
r.raise_for_status()
job_id = r.json()["id"]
print(f"Queued: {job_id}")
# 2. Poll
for _ in range(30):
time.sleep(10)
data = requests.get(f"{BASE}/videos/generations/{job_id}", headers=H, timeout=15).json()
if data["status"] == "succeeded":
video_url = data["output"]["url"]
# 3. Download
with requests.get(video_url, stream=True, timeout=60) as dl:
dl.raise_for_status()
pathlib.Path(dest).write_bytes(dl.content)
print(f"Saved: {dest}")
return dest
if data["status"] == "failed":
raise RuntimeError(data.get("error"))
raise TimeoutError("Generation timed out after 300s")
# Example usage
i2v_pipeline(
image_url="https://cdn.example.com/chair.jpg",
prompt="Camera slowly orbits around the chair, warm studio lighting",
dest="chair-orbit.mp4",
)
FAQ
Can I send a base64-encoded image instead of a URL?
Yes — replace image_url with image_base64 and pass the base64 string. HTTPS URLs are preferred in production to keep request payloads small.
What image aspect ratios produce the most stable motion?
For product shots, 1:1 and 4:3 images tend to produce the most stable motion. Veo 3.1 handles portrait formats well for fashion items; Grok 1.5 performs most predictably with landscape or square inputs.
Does GenRelay support Veo 3.1's reference-video mode?
Yes — ref2v is a separate endpoint that takes both a source image and a reference video to guide motion style. See the Veo 3.1 image-to-video guide for ref2v parameters.
What happens if my image has a transparent background?
Both models accept RGBA PNGs. Veo 3.1 typically renders transparency as a neutral background; Grok 1.5 may fill it with white. Test with your specific assets before running a large batch.
How do I run multiple clips in parallel without hitting rate limits?
Cap concurrent submissions with asyncio.Semaphore. The AI video API rate limits guide has a full asyncio queue implementation with 429 handling and automatic backoff.
Pricing as of August 2026. Check genrelay.ai/pricing for current rates.