Docs
Video generation
Text-to-video and image-to-video on Google's Veo 3.1 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. aspect_ratio is optional.
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",
"aspect_ratio": "16:9",
},
timeout=600, # video generation can take minutes
)
print(resp.json()["data"][0]["url"])duration (seconds) sets the clip length on the models billed per second: the Seedance models and Wan 2.7. The Veo models ignore it; every Veo clip is 8 seconds, billed per video.
# The models billed per second take a clip length: here 5 seconds of
# Seedance 2. Leave "duration" out and these models make a 5-second clip.
resp = requests.post(
"https://api.kunavo.com/v1/video/generations",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": "seedance-2",
"prompt": "a paper crane unfolds into a flock of birds over a city at dawn",
"duration": 5,
"aspect_ratio": "16:9",
},
timeout=600,
)
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",
},
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",
},
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 on the per-second models, 5 when omitted: Seedance 2, Seedance 2 Fast and Seedance 2.0 Mini take 4–15, Seedance 2.5 takes 4–30, Wan 2.7 takes 2–15. A length outside a model's range is refused with a 400. Veo ignores it: every Veo clip is 8 seconds. |
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 — see Raw input below. |
Raw input escape hatch
On the models billed per second, input is sent upstream as written, in place of the input our adapter would build. The Veo models ignore it.
The fields that set the price are the exception: resolution is always sent as one of the model's tiers (the default tier when it is left out or is not one of them), and duration has to be a whole number of seconds in the model's range (anything else is refused with a 400; 5 is sent when it is left out). The request's own resolution and duration are not read when input is set.
Video models
| Slug | Provider | Notes |
|---|---|---|
veo-3 | Veo 3.1 Fast — faster and cheaper than Quality, native audio. | |
veo-3-quality | Veo 3.1 Quality — top fidelity. | |
veo-3-lite | Veo 3.1 Lite — budget tier for previews. | |
seedance-2-5 | ByteDance | Seedance 2.5 — clips up to 30s, multimodal references, per-second billing. |
seedance-2 | ByteDance | Seedance 2 — multi-shot consistency, up to 1080p, per-second billing. |
seedance-2-fast | ByteDance | Seedance 2 Fast — budget tier, up to 720p, per-second billing. |
seedance-2-mini | ByteDance | Seedance 2.0 Mini — cheapest tier, 480p/720p only, for drafts and volume. |
wan-2-7 | Alibaba | Wan 2.7 — cinematic text-to-video, per-second billing. |
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.1 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}. The task object they return uses the field names and status values of OpenAI's video object, and adds output with the clip's URL. The request is a JSON body with the parameters above.
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",
"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"])From the OpenAI SDK, send that body with client.post and poll with client.get, as below. The SDK's own client.videos.create does not work here: it takes only Sora's parameters, and it posts them as a multipart form (in the Python SDK since version 2.27.0), which this endpoint refuses with a 400.
import time
from openai import OpenAI
client = OpenAI(api_key=API_KEY, base_url="https://api.kunavo.com/v1")
# The same JSON body, sent with the client's own post().
task = client.post(
"/videos",
body={
"model": "veo-3",
"prompt": "a cinematic dolly-in on a red origami crane",
"aspect_ratio": "16:9",
},
# object hands back the JSON as parsed. Passing dict instead raises
# after the task has been created.
cast_to=object,
options={"headers": {"Idempotency-Key": "my-task-uuid"}},
)
while task["status"] not in ("completed", "failed"):
time.sleep(5)
task = client.get(f"/videos/{task['id']}", cast_to=object)
if task["status"] == "failed":
raise RuntimeError(task["error"]["message"])
print(task["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, 30 days after submission: the task can be polled at least until then. Its result URLs do not expire. |
progress | int | 0–100. We currently report 0 until terminal, then 100. |
output | object | null | {url, urls[], duration_ |
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/ | 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 |
| Response shape | OpenAI /images/ | OpenAI /videos 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.