Docs

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

ParamTypeNotes
modelstring (required)A video model slug from /v1/models.
promptstring (required)Text description of the shot.
image_urlstringSingle source image for image-to-video. URL or data: URI.
image_urlsstring[]Multiple source images (Veo). Takes precedence over image_url.
image_modestringVeo: "frame" (default — first/last frame) or "reference" (style refs).
durationintClip 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_ratiostringe.g. "16:9", "9:16", "1:1".
resolutionstringWhere the model exposes tiers — e.g. "720p", "1080p".
inputobjectEscape 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

SlugProviderNotes
veo-3GoogleVeo 3.1 Fast — faster and cheaper than Quality, native audio.
veo-3-qualityGoogleVeo 3.1 Quality — top fidelity.
veo-3-liteGoogleVeo 3.1 Lite — budget tier for previews.
seedance-2-5ByteDanceSeedance 2.5 — clips up to 30s, multimodal references, per-second billing.
seedance-2ByteDanceSeedance 2 — multi-shot consistency, up to 1080p, per-second billing.
seedance-2-fastByteDanceSeedance 2 Fast — budget tier, up to 720p, per-second billing.
seedance-2-miniByteDanceSeedance 2.0 Mini — cheapest tier, 480p/720p only, for drafts and volume.
wan-2-7AlibabaWan 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

FieldTypeNotes
idstringvid_-prefixed task id. Stable across upstream changes.
objectstring"video".
statusstringqueued | in_progress | completed | failed.
modelstringThe model slug you submitted.
created_atintUnix seconds when we accepted the submit.
completed_atint | nullUnix seconds when terminal — null while pending.
expires_atintUnix seconds, 30 days after submission: the task can be polled at least until then. Its result URLs do not expire.
progressint0–100. We currently report 0 until terminal, then 100.
outputobject | null{url, urls[], duration_seconds, resolution} on completed. duration_seconds is the clip length requested from the model, null on Veo; resolution is the clip's resolution tier, or null when none was recorded.
errorobject | 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/videos returns 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/generationsAsync /v1/videos
Best forOne-off scripts, notebooksProduction, mobile, long-running jobs
NetworkHolds connection up to 9 minSubmit in seconds; poll on your schedule
Failure recoveryLose the response = lose the taskRe-query the id any time before expires_at
ConcurrencyOne server worker per callSubmits release immediately
Response shapeOpenAI /images/generations shapeOpenAI /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.

Where to go next