Video
Video takes longer than an HTTP timeout allows, so it works as create → poll → download.
1. Create a job
Section titled “1. Create a job”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)import OpenAI from 'openai'
const client = new OpenAI({ baseURL: 'https://api.mixen.ai/v1', apiKey: process.env.MIXEN_API_KEY })const video = await client.videos.create({ model: 'seedance-2.5', prompt: 'A drone circles a lighthouse on a rocky shore at sunrise', seconds: '5', size: '1280x720',})console.log(video.id, video.status)curl https://api.mixen.ai/v1/videos \ -H "Authorization: Bearer $MIXEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.5", "prompt": "A drone circles a lighthouse on a rocky shore at sunrise", "seconds": "5", "size": "1280x720" }'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.
2. Poll the job
Section titled “2. Poll the job”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.
Skip polling: webhook, wait, idempotency
Section titled “Skip polling: webhook, wait, idempotency”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', ''))3. Download the result
Section titled “3. Download the result”curl -L https://api.mixen.ai/v1/videos/video_.../content \ -H "Authorization: Bearer $MIXEN_API_KEY" \ -o result.mp4The endpoint returns a finished mp4.
The whole cycle in a single OpenAI SDK script (the same path shown in the /models catalog snippets):
import timefrom 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")Image-to-video
Section titled “Image-to-video”{ "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.
Upscaling
Section titled “Upscaling”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=0upscale_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.
Price before you run
Section titled “Price before you run”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.