Skip to content
RU

Video

Video takes longer than an HTTP timeout allows, so it works as create → poll → download.

from openai import OpenAI
client = OpenAI(base_url="https://api.mixen.ai/v1", api_key=API_KEY)
video = client.videos.create(
model="seedance-2.5",
prompt="A drone circles a lighthouse on a rocky shore at sunrise",
seconds="5",
size="1280x720",
)
print(video.id, video.status)

The response comes back immediately with queued:

{"id": "video_...", "object": "video", "model": "bytedance/seedance-2.5", "status": "queued"}
Field Description
model A catalog model (veo-3.1-fast by default). The sora-2 / sora-2-pro aliases were retired by OpenAI on 2026-09-24 and redirect to veo-3.1-fast with warnings in the response — see deprecated models
prompt Scene description, required. The length cap is the prompt_max_chars field in the catalog: vendors differ a lot (Kling 2,500, Grok Imagine 4,096, MiniMax H3 7,000 characters), 30,000 for the rest
seconds Duration; accepted values are the seconds field in the catalog
size WxH; accepted values are the sizes field in the catalog
input_reference Image-to-video: {"image_url": "data:image/png;base64,..."}
input_references Reference-to-video: from 2 images; the cap is the max_references field in the catalog (30 for Seedance 2.5, 7 for Grok Imagine); only on models with references: true
seed Reproducibility seed: an integer in 0…2147483647, on models with seed: true in the catalog. The same prompt with the same seed yields the same clip; without it the upstream picks a random one

input_reference accepts data: URLs only — external links will not work, so base64-encode the image.

curl https://api.mixen.ai/v1/videos/video_... \
-H "Authorization: Bearer $MIXEN_API_KEY"

Statuses run queued → in_progress → completed or failed. Poll every 5–10 seconds; faster gains nothing and spends your rate limit.

A job stuck for more than 20 minutes is marked failed on the next status poll — the repair fires on GET /videos/{id}, not from a background process. Such a job is not charged.

Three optional body fields of POST /v1/videos, shared by video and music:

Field What it does
webhook_url A public https URL. On completed or failed we POST the same job object that GET returns. Private hosts and http:// are rejected with 400 invalid_webhook_url before the job is created
wait true holds the request until the job is terminal and returns the final object (up to wait_timeout, default 60 s, max 300). On timeout the job is returned as is (in_progress), no error
idempotency_key Up to 128 chars. A repeated request with the same key returns the already created job instead of a new one (and does not charge again); the response carries X-Mixen-Idempotent-Replay: true. Unique per account

Webhook request headers:

Header Value
X-Mixen-Event video.completed or video.failed
X-Mixen-Delivery the job id
X-Mixen-Signature t=<unix>,v1=<hex> — HMAC-SHA256 over "<t>.<raw body>"; the secret is the sha256 hex of your API key (we never store the key itself, you compute the hash locally)

Your endpoint must answer 2xx; otherwise delivery is retried after 5, 20, 60 and 180 seconds. At-least-once: handle repeats idempotently by X-Mixen-Delivery. Verifying the signature:

import hashlib, hmac
def verify(raw_body: bytes, header: str, api_key: str) -> bool:
secret = hashlib.sha256(api_key.encode()).hexdigest()
parts = dict(kv.split('=', 1) for kv in header.split(','))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get('v1', ''))
curl -L https://api.mixen.ai/v1/videos/video_.../content \
-H "Authorization: Bearer $MIXEN_API_KEY" \
-o result.mp4

The endpoint returns a finished mp4.

The whole cycle in a single OpenAI SDK script (the same path shown in the /models catalog snippets):

import time
from openai import OpenAI
client = OpenAI(base_url="https://api.mixen.ai/v1", api_key=API_KEY)
video = client.videos.create(
model="seedance-2.5",
prompt="A drone circles a lighthouse on a rocky shore at sunrise",
seconds="5",
size="1280x720",
)
while video.status not in ("completed", "failed"):
time.sleep(5)
video = client.videos.retrieve(video.id)
client.videos.download_content(video.id).write_to_file("video.mp4")
{
"model": "happyhorse-1.1",
"prompt": "The camera slowly pushes in, wind moves the hair",
"input_reference": {"image_url": "data:image/jpeg;base64,/9j/4AAQ..."}
}

Worth knowing: models in this class inherit the aspect ratio of the source image and ignore an explicit size. If you want vertical video, feed a vertical reference.

POST /v1/videos/upscale takes multipart/form-data with a finished file:

curl https://api.mixen.ai/v1/videos/upscale \
-H "Authorization: Bearer $MIXEN_API_KEY" \
-F video=@clip.mp4 \
-F upscale_factor=2 \
-F creativity=0

upscale_factor defaults to 1.5 and creativity to 0: at zero the upscaler preserves the frame as-is, higher values invent detail and can alter faces. Input video is limited to 64 MB.

Upscaling is an async job too — the response is status: queued, then poll and download through the same /videos/{id} endpoints.

Video is billed per second: per_second_rub from the catalog times the actual duration. Resolution moves the price, so 720p costs more than 480p on the same model. Check the catalog before putting generation in a loop.

POST /v1/videos/price accepts the same body as POST /v1/videos and returns the amount without generating or charging anything. It uses the same formula as the actual charge (resolution, duration, references), so the quote matches the bill; validation is shared too — invalid parameters return the same 400/404 as a real run.

{
"object": "price",
"model": "veo-3.1-fast",
"model_id": "google/veo-3.1-fast",
"price": {"rub": 107.34, "usd": 1.2},
"per_unit": {"rub": 107.34, "unit": "video"},
"quantity": 1,
"params": {"seconds": 8, "size": "1280x720", "aspect_ratio": "16:9", "resolution": "720p", "input_reference": false, "input_references": 0}
}

The rouble amount uses the live rate — quote right before you run, don’t cache it.