Docs
Video generation
Text-to-video and image-to-video on Google's Veo 3 family. Two API shapes: a one-shot synchronous endpoint that blocks until the clip is ready, and a submit-and-poll async pair for production workloads.
Endpoints: POST /v1/video/generations (synchronous, covered first), and POST /v1/videos + GET /v1/videos/{id} (async, recommended for production). Generation runs 30 seconds to several minutes depending on model and duration — for the sync endpoint, set a generous client timeout (600s is safe).
Text-to-video
Pass a prompt and pick a model. Optional duration (seconds) and aspect_ratio.
import requests
resp = requests.post(
"https://api.kunavo.com/v1/video/generations",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "veo-3",
"prompt": "a cinematic dolly-in on a red origami crane unfolding, soft light",
"duration": 5,
"aspect_ratio": "16:9",
},
timeout=600, # video generation can take minutes
)
print(resp.json()["data"][0]["url"])Image-to-video
Add an image_url and Veo animates from it — by default the still becomes the video's first frame.
# Image-to-video: animate a still image. The source image can be an
# https URL, or inline base64 — Kunavo hosts it for the model automatically.
resp = requests.post(
"https://api.kunavo.com/v1/video/generations",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "veo-3",
"prompt": "the crane gently lifts off the desk",
"image_url": "https://your-cdn.com/source.png",
"duration": 5,
},
timeout=600,
)
print(resp.json()["data"][0]["url"])image_url accepts an https:// URL or an inline data: base64 URI. base64 is decoded and hosted on Kunavo's CDN automatically before the model runs.First frame, last frame, and references
image_mode controls how Veo uses the image(s). Pass image_urls (an array) to send more than one.
frame(default) — one image is the first frame; two images become the first and last frame, and Veo generates the transition between them.reference— up to three images act as style and subject references. They steer the look but never appear as a frame.
# First + last frame: pass two images and Veo fills in the motion.
resp = requests.post(
"https://api.kunavo.com/v1/video/generations",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "veo-3",
"prompt": "a smooth transition between the two poses",
"image_urls": [
"https://your-cdn.com/first.png",
"https://your-cdn.com/last.png",
],
"image_mode": "frame",
"duration": 5,
},
timeout=600,
)Uploading a source frame
When you only have a local file, upload it once via POST /v1/files and reuse the returned permanent URL.
# No public URL? Upload the still first, then animate it.
with open("frame.png", "rb") as f:
up = requests.post(
"https://api.kunavo.com/v1/files",
headers={"Authorization": f"Bearer {API_KEY}"},
files={"file": f},
).json()
resp = requests.post(
"https://api.kunavo.com/v1/video/generations",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "veo-3",
"prompt": "slow push-in, volumetric light",
"image_url": up["url"],
},
timeout=600,
)Parameters
| Param | Type | Notes |
|---|---|---|
model | string (required) | A video model slug from /v1/models. |
prompt | string (required) | Text description of the shot. |
image_url | string | Single source image for image-to-video. URL or data: URI. |
image_urls | string[] | Multiple source images (Veo). Takes precedence over image_url. |
image_mode | string | Veo: "frame" (default — first/last frame) or "reference" (style refs). |
duration | int | Clip length in seconds. Model-dependent (commonly 5–10). |
aspect_ratio | string | e.g. "16:9", "9:16", "1:1". |
resolution | string | Where the model exposes tiers — e.g. "720p", "1080p". |
input | object | Escape hatch — raw upstream input, bypasses our adapter. |
Video models
| Slug | Provider | Notes |
|---|---|---|
veo-3 | Veo 3 Fast — cheapest tier, native audio. | |
veo-3-quality | Veo 3 Quality — top fidelity. | |
veo-3-lite | Veo 3 Lite — budget tier for previews. |
Always check /models for the live list and per-model pricing.
Latency & timeouts
- Kunavo polls the upstream task for up to 540 seconds before returning a 504.
- Fast tiers (Veo 3 Fast / Lite): often 30–90s.
- Quality tiers and longer durations: 2–5 minutes.
- Set your HTTP client timeout to at least 600s.
- On flaky networks or mobile clients, prefer the async API instead — the submit returns in seconds and you poll for the result.
Async API (submit + poll)
Endpoints: POST /v1/videos + GET /v1/videos/{id}. Shape mirrors OpenAI's Sora API so SDKs can swap base URLs.
The sync endpoint above holds an HTTP connection open for up to nine minutes — convenient for one-shot scripts but unreliable on mobile and inefficient on the server. The async pair returns immediately with a vid_* id; you poll GET /v1/videos/{id} until status is completed or failed. Same models, same pricing, same archived URLs.
import requests, time
# 1. Submit — returns immediately with a task id. No long-lived connection.
submit = requests.post(
"https://api.kunavo.com/v1/videos",
headers={
"Authorization": f"Bearer {API_KEY}",
# Optional: retrying with the same key inside ~24h returns the
# original task rather than submitting again.
"Idempotency-Key": "my-task-uuid",
},
json={
"model": "veo-3",
"prompt": "a cinematic dolly-in on a red origami crane",
"duration": 5,
"aspect_ratio": "16:9",
},
timeout=60,
).json()
task_id = submit["id"] # "vid_abc..."
status = submit["status"] # "queued"
# 2. Poll until terminal. Recommended cadence: 5s, backing off to 30s.
while status not in ("completed", "failed"):
time.sleep(5)
r = requests.get(
f"https://api.kunavo.com/v1/videos/{task_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
).json()
status = r["status"]
if status == "failed":
raise RuntimeError(r["error"]["message"])
print(r["output"]["url"])Response shape
| Field | Type | Notes |
|---|---|---|
id | string | vid_-prefixed task id. Stable across upstream changes. |
object | string | "video". |
status | string | queued | in_progress | completed | failed. |
model | string | The model slug you submitted. |
created_at | int | Unix seconds when we accepted the submit. |
completed_at | int | null | Unix seconds when terminal — null while pending. |
expires_at | int | Unix seconds after which the result is pruned (~30 days). |
progress | int | 0–100. We currently report 0 until terminal, then 100. |
output | object | null | {url, urls[], duration_seconds, resolution} on completed. |
error | object | null | {code, message} on failed. |
Headers & conventions
Idempotency-Key(optional, up to 128 chars) — submitting twice with the same key on the same account returns the original task instead of creating a duplicate. Useful for retry loops.POST /v1/videosreturns 202 Accepted on a fresh submit and 200 OK when an idempotency key hit replays a previous result.GET /v1/videos/{id}is owner-scoped: querying a task that belongs to another account returns 404 — we never confirm an id exists for someone else.- Recommended polling cadence: 5 s with exponential backoff up to 30 s. Tasks usually settle in 30 s – 5 min depending on model and resolution.
When to use which
| Sync /v1/video/generations | Async /v1/videos | |
|---|---|---|
| Best for | One-off scripts, notebooks | Production, mobile, long-running jobs |
| Network | Holds connection up to 9 min | Submit in seconds; poll on your schedule |
| Failure recovery | Lose the response = lose the task | Re-query the id any time before expires_at |
| Concurrency | One server worker per call | Submits release immediately |
| SDK swap | OpenAI /images/generations shape | OpenAI /videos (Sora) shape |
Response & storage
Response is OpenAI's standard shape — data[].url. The URL is a permanent files.kunavo.com link: Kunavo archives every result to its own CDN, so unlike raw upstream URLs it never expires. Every clip also shows up in /app/assets.