Video generation
How to use the video API (submit, check progress, download the file), the fields and values each model accepts, how tasks are charged, how long files can be downloaded, and errors.
Generating a video takes time, so a single call does not return the video. You use it as a task (a job you submit and whose result you collect after generation finishes), in three steps.
| Step | Method and path | What it does |
|---|---|---|
| 1. Submit | POST /v1/videos | Send the model, the prompt (a description of the video you want), the length and the resolution. The task ID is returned |
| 2. Check progress | GET /v1/videos/{task ID} | Repeat at intervals until status becomes completed or failed |
| 3. Download the file | GET /v1/videos/{task ID}/content | Returns the finished video as an MP4 file |
The base URL and the way you send the key are the same as for the other OpenAI-format endpoints (https://token.echohub.io/v1, Authorization: Bearer key).
The models currently offered are listed in the model list.
Models
| Model ID | Provider | Length | Resolution | Inputs you can use |
|---|---|---|---|---|
MiniMax-H3 | MiniMax | [ ] | 768P, 2K | [ ] |
doubao-seedance-2-0-260128 (Seedance 2.0) | ByteDance (Volcano Engine) | [ ] | 480p, 720p, 1080p, 4k | [ ] |
- Prices (per second or per 1 million tokens) are listed on each model's page in the model list
- The processing region is shown on each model's page in the model list. The prompt and the images, videos and audio used as references are passed to the provider's API that processes them in that region (Data handling)
- The fields and values you can use differ by model. See "Parameters by model" below
Example
This example makes a 6-second 768P video with MiniMax-H3 and saves it as video.mp4. Put your key in the environment variable ECHO_API_KEY first (Quickstart).
# 1. Submit. Note the id that is returned
curl "https://token.echohub.io/v1/videos" \
-H "Authorization: Bearer $ECHO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3",
"prompt": "夕方の海辺を、柴犬がゆっくり歩いている",
"seconds": 6,
"resolution": "768P"
}'
# 2. Check progress. Set VIDEO_ID to the id returned in step 1
VIDEO_ID="task_..."
curl "https://token.echohub.io/v1/videos/$VIDEO_ID" \
-H "Authorization: Bearer $ECHO_API_KEY"
# 3. When status is completed, download the file
curl -o video.mp4 "https://token.echohub.io/v1/videos/$VIDEO_ID/content" \
-H "Authorization: Bearer $ECHO_API_KEY"# Create and activate a virtual environment
python3 -m venv .venv && source .venv/bin/activate
pip install requestsimport os
import time
import requests
BASE = "https://token.echohub.io/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['ECHO_API_KEY']}"}
def check(res):
# On an error, print the HTTP status and the error body, then stop
if not res.ok:
raise SystemExit(f"HTTP {res.status_code}: {res.text}")
return res
# 1. Submit
video = check(requests.post(f"{BASE}/videos", headers=HEADERS, json={
"model": "MiniMax-H3",
"prompt": "夕方の海辺を、柴犬がゆっくり歩いている",
"seconds": 6,
"resolution": "768P",
})).json()
print("Submitted:", video["id"])
# 2. Check progress every 15 seconds until the task completes or fails
while video["status"] in ("queued", "in_progress"):
time.sleep(15)
video = check(requests.get(f"{BASE}/videos/{video['id']}", headers=HEADERS)).json()
print(video["status"], video["progress"])
if video["status"] != "completed":
raise SystemExit(f"Generation failed: {video.get('error')}")
# 3. Download the file
res = check(requests.get(f"{BASE}/videos/{video['id']}/content", headers=HEADERS))
with open("video.mp4", "wb") as f:
f.write(res.content)
print("Saved to video.mp4")Save it as video.py and run it with python video.py.
import { writeFile } from "node:fs/promises";
const BASE = "https://token.echohub.io/v1";
const auth = { Authorization: `Bearer ${process.env.ECHO_API_KEY}` };
// On an error, print the HTTP status and the error body, then stop
async function check(res) {
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
return res;
}
// 1. Submit
const submitted = await check(
await fetch(`${BASE}/videos`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({
model: "MiniMax-H3",
prompt: "夕方の海辺を、柴犬がゆっくり歩いている",
seconds: 6,
resolution: "768P",
}),
}),
);
let video = await submitted.json();
console.log("Submitted:", video.id);
// 2. Check progress every 15 seconds until the task completes or fails
while (video.status === "queued" || video.status === "in_progress") {
await new Promise((resolve) => setTimeout(resolve, 15_000));
const res = await check(await fetch(`${BASE}/videos/${video.id}`, { headers: auth }));
video = await res.json();
console.log(video.status, video.progress);
}
if (video.status !== "completed") throw new Error(`Generation failed: ${JSON.stringify(video.error)}`);
// 3. Download the file
const res = await check(await fetch(`${BASE}/videos/${video.id}/content`, { headers: auth }));
await writeFile("video.mp4", Buffer.from(await res.arrayBuffer()));
console.log("Saved to video.mp4");Requires Node.js 18 or later. No packages to install. Save it as video.mjs and run it with node video.mjs.
1. Submit
Send the following fields as JSON to POST /v1/videos.
| Parameter | Required | Description |
|---|---|---|
model | Required | The model ID |
prompt | Required for MiniMax-H3 | A description of the video you want |
seconds | Length of the video in seconds. An integer | |
resolution | Resolution (MiniMax-H3). For Seedance 2.0, write it in metadata.resolution | |
images | An array of image URLs, used for the first and last frame (the first and last picture of the video) (MiniMax-H3) | |
input_reference | The URL of an image to use as the first frame (MiniMax-H3) | |
metadata | Extra settings for each model (Seedance 2.0's resolution, aspect ratio, sound on or off, reference material and so on). A JSON object |
- Pass images, videos and audio as public URLs that the provider's servers can fetch
- Each model decides where each value goes. Values written anywhere other than the place given in "Parameters by model" may not be used. Echo refuses requests that would produce a video different from what you asked for or a price that does not match, such as a length, resolution or image written where Seedance 2.0 does not read it (HTTP 400, "Requests that are rejected")
- Do not use the OpenAI-format
size(such as1280x720). It does not set the aspect ratio; some models read it as the resolution and others return an error - For MiniMax-H3 you can also attach an image file to the request: use the form format (
multipart/form-data) and attach one file asinput_reference(up to 20 MB). In the form format, writemetadataas a JSON string. Seedance 2.0 does not accept files (HTTP 400); pass images as URLs
When the submission is accepted, JSON like the following is returned with HTTP 200.
{"id":"task_3H0YkK1PMgAiKglDcHbxqtL8WC7C247u","object":"video","model":"MiniMax-H3","status":"queued","progress":0,"created_at":1790796502}An estimated price is deducted from your balance at submission ("Pricing" below).
2. Check progress
GET /v1/videos/{task ID} returns the state of the task.
| Field | Description |
|---|---|
id | The task ID (a string starting with task_) |
object | Always video |
model | The model ID |
status | The state (table below) |
progress | A number from 0 to 100, fixed for each state (0 right after submission, 10 (Seedance 2.0) or 30 (MiniMax-H3) while waiting, 50 while generating, 100 when finished). It does not measure actual progress |
created_at | When the task was submitted (seconds since 1970-01-01) |
completed_at | When the task completed. Present only when it has completed |
error | Why the task failed (code and message). Present only when the provider returned a reason (Seedance 2.0). Not present for MiniMax-H3 |
status | Meaning |
|---|---|
queued | Waiting in the provider's queue |
in_progress | Being generated |
completed | Completed. You can download the file |
failed | Failed. The price is refunded in full |
- Echo's servers ask the provider for progress every 15–30 seconds, so it can take about 30 seconds after the provider finishes before the status becomes
completed. The interval is sometimes longer than 30 seconds - Check progress every 10–15 seconds or so. Echo does not send a notice when a task completes
- A task that has not completed 24 hours after submission is set to
failedand refunded in full. For Seedance 2.0, the provider cancels a task that has not completed 23 hours after submission; it then becomesfailedand is refunded in full
3. Download the file
GET /v1/videos/{task ID}/content returns the finished video as an MP4 file (Content-Type: video/mp4). You can also use HEAD to check only the file size, or the Range header to download part of the file.
- Echo does not store generated videos. Each time you call this path, Echo fetches the file from the provider and passes it to you as is
- You can download the file only while the provider keeps it. After that, HTTP 410 is returned. Download and save the file soon after the task completes
| Model | How long the provider keeps the file |
|---|---|
| Seedance 2.0 | [ ] |
| MiniMax-H3 | [ ] |
Parameters by model
MiniMax-H3
| Item | Field | Values |
|---|---|---|
| Length | seconds | An integer ([ ]). Default 5 |
| Resolution | resolution | 768P or 2K. Default 768P |
| Aspect ratio | metadata.ratio | 16:9, 9:16, 1:1, 4:3, 3:4, 21:9, adaptive (follow the input image or video). Default: adaptive when you pass an image or video, 16:9 otherwise. adaptive cannot be used without an image or video |
| First and last frame | images | 1 or 2 image URLs. The first is the first frame and the second is the last frame. An image in input_reference becomes the first frame |
| Reference images | metadata.content | Up to 9, in the form shown in the example below |
| Reference videos | metadata.reference_video | An array of URLs. Up to 3 |
- Frames (
images,input_reference) and references (images, videos) cannot be used together - Reference audio cannot be used (HTTP 400)
- Without
prompt, HTTP 400 is returned
An example with a reference image:
{
"model": "MiniMax-H3",
"prompt": "画像のマグカップが、木のテーブルの上でゆっくり回っている",
"seconds": 5,
"resolution": "768P",
"metadata": {
"content": [
{"type": "image_url", "image_url": {"url": "https://example.com/mug.png"}, "role": "reference_image"}
]
}
}| Type | Accepted files (provider's specification) |
|---|---|
| Image | JPG, PNG, WEBP, HEIC, HEIF. Up to 30 MB each. Width and height 256–5760 pixels, width ÷ height 0.4–2.5 |
| Video | MP4, MOV. Up to 50 MB each. 2–15 seconds each, 15 seconds in total |
| Whole request | Up to 64 MB. Pass large files as URLs |
Seedance 2.0 (doubao-seedance-2-0-260128)
| Item | Field | Values |
|---|---|---|
| Length | seconds | An integer ([ ]) |
| Resolution | metadata.resolution | 480p, 720p, 1080p, 4k. Default 720p |
| Aspect ratio | metadata.ratio | 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, adaptive. Default adaptive (the model chooses from the prompt or images) |
| Sound | metadata.generate_audio | true (with sound) or false (no sound). Default true |
| First frame, first and last frame, references | metadata.content | In the form shown in the example below. role decides how each item is used |
| Maximum wait at the provider | metadata.execution_expires_after | A whole number of seconds from 3600 to 82800. When omitted, Echo sets 82800 (23 hours). The provider cancels a task that has not completed within this time, and the price is refunded in full |
- Write the length in
secondsand the resolution inmetadata.resolution. The top-levelresolution,durationandinput_reference, and parameters such as--dur 5after the prompt, cannot be used with Seedance 2.0 (HTTP 400, "Requests that are rejected") - Do not omit
secondsandmetadata.resolution. When they are omitted, the estimated price is calculated as 15 seconds at 1080p, so you may not be able to submit because of your balance (the price is recalculated from actual usage after generation) - First frame, first and last frame, and references (images, videos, audio) cannot be combined (provider's specification)
- Pass images, videos and audio as URLs. Attaching a file in the form format returns HTTP 400
- Images and videos showing real people's faces cannot be used as references (provider's specification)
An example with a first and last frame:
{
"model": "doubao-seedance-2-0-260128",
"prompt": "つぼみがゆっくり開いて花が咲く",
"seconds": 5,
"metadata": {
"resolution": "720p",
"content": [
{"type": "image_url", "image_url": {"url": "https://example.com/first.png"}, "role": "first_frame"},
{"type": "image_url", "image_url": {"url": "https://example.com/last.png"}, "role": "last_frame"}
]
}
}role | Use | Count |
|---|---|---|
first_frame | First frame (type is image_url). On its own, the video starts from this image | 1 |
last_frame | Last frame (type is image_url). Use it together with first_frame | 1 |
reference_image | Reference image (type is image_url) | 1–9 |
reference_video | Reference video (type is video_url, URL in video_url.url) | Up to 3 |
reference_audio | Reference audio (type is audio_url, URL in audio_url.url). Use it together with an image or video | Up to 3 |
| Type | Accepted files (provider's specification) |
|---|---|
| Image | JPEG, PNG, WEBP, BMP, TIFF, GIF, HEIC, HEIF. Under 30 MB each. Width and height 300–6000 pixels, width ÷ height 0.4–2.5 |
| Video | MP4, MOV. Up to 200 MB each. 2–15 seconds each, 15 seconds in total |
| Audio | WAV, MP3. Up to 15 MB each. 2–15 seconds each, 15 seconds in total |
| Whole request | Up to 64 MB |
Requests that are rejected
Echo refuses the following at submission, because the video would not match what you asked for, or the price would be calculated from different values than the ones used to generate the video. They return HTTP 400 with error.code invalid_request, and message says how to write the request instead. They are free.
| What was written | Model | Write instead | message |
|---|---|---|---|
The length in duration or metadata.duration (without seconds, or with a different value from seconds) | Seedance 2.0 | seconds | Write the video length in seconds, for example "seconds": 5. This model does not read duration. With a value different from seconds: duration does not match seconds. Write the video length only in seconds |
A seconds value that is not a whole number (such as 5.5) | Seedance 2.0 | A whole number (5 or "5") | seconds must be a whole number of seconds, for example "seconds": 5 |
The length as frames or metadata.frames (a number of frames) | Seedance 2.0 | seconds | This model does not read frames. Write the video length in seconds, for example "seconds": 5 |
The resolution in the top-level resolution | Seedance 2.0 | metadata.resolution | Write the resolution in metadata.resolution, for example "metadata": {"resolution": "720p"}. This model does not read the top-level resolution |
Only size, without metadata.resolution | Seedance 2.0 | metadata.resolution | Write the resolution in metadata.resolution, for example "metadata": {"resolution": "720p"}. This model does not read size |
A metadata.resolution other than 480p, 720p, 1080p and 4k (such as 720P) | Seedance 2.0 | One of these four | metadata.resolution must be one of 480p, 720p, 1080p, 4k |
Length or resolution parameters after the prompt (--dur 5, --rs 720p and so on) | Seedance 2.0 | seconds and metadata.resolution | Write the video length in seconds and the resolution in metadata.resolution. This model does not accept --dur in the prompt |
Images in input_reference, image or images | Seedance 2.0 | metadata.content (for the first frame, set role to first_frame) | This model does not read input_reference. Put images in metadata.content; for the first frame use {"type": "image_url", "image_url": {"url": "<image URL>"}, "role": "first_frame"} |
| A file attached in the form format | Seedance 2.0 | Image URLs in metadata.content | This model does not accept uploaded files. Pass images as URLs in metadata.content; for the first frame use {"type": "image_url", "image_url": {"url": "<image URL>"}, "role": "first_frame"} |
A metadata.execution_expires_after outside 3600–82800 | Seedance 2.0 | A whole number from 3600 to 82800 (when omitted, Echo sets 82800) | metadata.execution_expires_after must be a whole number of seconds from 3600 to 82800 (1 to 23 hours) |
Reference audio (metadata.reference_audio, audio_url in metadata.content) | MiniMax-H3 | Not available | Reference audio is not supported for this model. Remove audio_url and reference_audio from the request |
A video model called through POST /v1/responses or POST /v1/tasks/… | Both | POST /v1/videos | Video generation requests must be sent to POST /v1/videos |
- In
message,duration,frames,input_referenceand--durare replaced with the name you actually wrote (metadata.duration,image,--rsand so on) - For a correctly written request, the only thing Echo adds is
metadata.execution_expires_afterfor Seedance 2.0 (the maximum time to wait at the provider). Everything else reaches the provider as written
Pricing
Video is charged at a unit price per second (MiniMax-H3) or per 1 million tokens (Seedance 2.0). Tokens are the unit the provider uses to count the amount of video. Unit prices are listed on each model's page in the model list.
The price of a task is settled in this order.
- When you submit, an estimated price is deducted from your balance. The estimate is calculated from the requested length and resolution (for MiniMax-H3, also the number of reference images and any reference video). If your balance or the key's spending limit is not enough, the submission is refused (HTTP 403)
- When generation finishes, the price is recalculated from the actual usage the provider reports, and the difference is refunded or additionally deducted
- Tasks that fail, and tasks that have not completed 24 hours after submission (23 hours for Seedance 2.0), are refunded in full
| Model | What the recalculation uses |
|---|---|
| Seedance 2.0 | The actual number of tokens. The unit price per 1 million tokens depends on whether you attached a reference video |
| MiniMax-H3 | The actual length, number of reference images and seconds of reference video. A reference video is counted as 15 seconds at submission |
- Requests refused at submission (wrong values, requests the provider did not accept and so on) are free
- An additional deduction is made even if your balance is not enough. The part that makes your balance negative is deducted from your next top-up (Pricing and top-ups)
- The state and price of each task are shown in 「呼び出し履歴」 (Call history) in the console (API keys and usage)
Errors
Error bodies use the same format as the other OpenAI-format endpoints (Errors). Errors specific to video generation are listed below.
| Case | HTTP status and error.code | message | What to do |
|---|---|---|---|
| A value such as the length or resolution cannot be used with the model | 400, invalid_request or an empty string | An English sentence giving the reason (for example MiniMax-H3 duration must be an integer between 4 and 15 seconds) | Fix it using "Parameters by model" |
The request is written in a way Echo refuses (for Seedance 2.0, the length in duration or a file attached in the form format; a video model called through POST /v1/responses, and so on) | 400, invalid_request | An English sentence saying how to write it (for example Write the video length in seconds, for example "seconds": 5. This model does not read duration) | Fix it using "Requests that are rejected" |
| The provider did not accept the request | 400, upstream_error | The model service rejected the request, check the parameters and try again | Check the values and the URLs of images and other files |
| Wrong task ID, or a task of another account | 400 (404 when downloading the file), task_not_found | Task not found | Check the ID |
| The file was requested before the task completed | 400, task_not_ready | The video is not ready yet | Download after the status becomes completed |
| The file of a failed task was requested | 400, task_failed | The video generation failed | Submit again |
| The provider no longer keeps the file | 410, artifact_gone | Artifact content is no longer available | The file cannot be downloaded. Submit again |
| Insufficient balance | 403, insufficient_quota | Insufficient balance | Top up |
| Not enough of the key's spending limit left | 403, insufficient_quota | This API key has reached its spending limit | Raise the spending limit (Limits and rate limits) |
| Wrong model ID | 503, model_not_found | The model {model ID} does not exist or is not available | Check the model ID |
Errors returned at submission are free.
Using the OpenAI SDK
- The OpenAI SDKs (Python, Node.js) also have video functions (
videos.createand so on), but in the SDKs they are marked as deprecated with the note "The Sora API is scheduled to permanently shut down on September 24, 2026". The examples on this page call the API over HTTP without the SDK - The SDK's
videos.createsends the request in the form format (multipart/form-data). In this format,metadatadoes not reach the provider unless it is a JSON string (Python example:extra_body={"metadata": json.dumps({"resolution": "720p"})})
Sources
| What | Source | Checked |
|---|---|---|
The three steps and responses, states and progress, downloading the file (including HEAD and Range), error bodies, deduction, recalculation and refunds, Seedance 2.0's metadata.resolution and metadata.content (role) reaching the provider, Seedance 2.0 refusing form-format files, metadata in the form format and in the SDK's videos.create | Measured in Echo's test environment, with the same relay settings as production and a server that returns test responses in place of the provider's API | 2026-10-02 |
| How often progress is checked (19–36 seconds from submission to the first check, then every 30 seconds) | Measured in Echo's test environment (as above) | 2026-10-01 |
| Tasks not completed within 24 hours are failed and refunded | Echo relay server settings (as of 2026-10-01) | 2026-10-01 |
Requests that are rejected and their errors; Echo setting Seedance 2.0's metadata.execution_expires_after to 82800 | Measured in Echo's test environment (as above) | 2026-10-02 |
The range of Seedance 2.0's metadata.execution_expires_after (3600–259200 seconds, 172800 when omitted); parameters such as --dur after the prompt | https://www.volcengine.com/docs/82379/1520757 | 2026-10-02 |
| MiniMax-H3 length, resolution, aspect ratio, input combinations and file conditions; the file URL has a time limit | https://platform.minimax.io/docs/api-reference/video-generation-v2-create , https://platform.minimax.io/docs/api-reference/video-generation-v2-query | 2026-10-01 |
Seedance 2.0 length, resolution, aspect ratio, sound, role and file conditions; files kept for 24 hours; real people's faces cannot be used as references | https://www.volcengine.com/docs/82379/1520757 , https://www.volcengine.com/docs/82379/1521309 | 2026-10-01 |
| The OpenAI SDKs' video functions are deprecated | https://github.com/openai/openai-python (3.23.0), https://github.com/openai/openai-node (7.27.0) | 2026-10-02 |