跳转到内容
免费注册
接口文档

视频 API:报价、生成、轮询与下载

通过 BetterToken API Key 调用十款视频模型,使用可恢复的 Python 示例完成报价、异步生成、轮询和 MP4 下载。

视频生成是异步任务。先查询模型能力并取得报价,再提交生成;收到任务 ID 后轮询,只有成片可用时才下载。

接入信息

项目值
Base URLhttps://bettertoken.ai/v1
认证Authorization: Bearer YOUR_API_KEY
查询可用模型GET /v1/video-models
生成前报价POST /v1/videos/quote
提交生成POST /v1/videos
查询任务GET /v1/videos/{id}
下载成片GET /v1/videos/{id}/content
兼容创建入口POST /v1/video/generations

查询可用模型

终端窗口
export BETTERTOKEN_API_KEY="YOUR_API_KEY"
curl --fail-with-body "https://bettertoken.ai/v1/video-models" \
-H "Authorization: Bearer $BETTERTOKEN_API_KEY"

目录中的每项由 id 与 operation 共同标识。按所选项的 fields、media_limits 和 constraints 填写请求;不同操作不能共用一套固定参数。

分系列起步设置

系列示例采用的设置
Seedance CN5 秒、720p、16:9,音频关闭
Kling 3.05 秒、16:9,标准模式;不传通用分辨率字段
MiniMax H35 秒、768p、16:9;音频沿用模型默认
Wan 3.0 / Prime5 秒、720p、16:9,音频关闭
Wan 2.7 T2V / I2V文生:5 秒、720p、16:9;图生:5 秒、720p,比例由素材决定

这些是脚本的起步组合,不是各模型的完整上限。切换系列时不要只替换模型名。十款精确 ID 和操作列表见 模型合集.

完整 Python 流程

使用 Python 3,无需安装额外依赖。先设置环境变量,再将下方代码存为 video_example.py 并运行。报价本身不启动生成;输入 GENERATE 后会提交真实任务,可能产生费用。

终端窗口
export BETTERTOKEN_API_KEY="YOUR_API_KEY"
export VIDEO_MODEL="wan-2.7-t2v"
export VIDEO_STATE_FILE="video-state.json"
python3 video_example.py

新任务使用新的 VIDEO_STATE_FILE。重试同一任务时,保留相同请求、BetterToken API Key 和状态文件;脚本会复用 Idempotency-Key 或已保存的任务 ID。脚本等待上限只是客户端设置,不是生成时长承诺。

import hashlib
import json
import os
import shutil
import time
import uuid
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen
base_url = "https://bettertoken.ai/v1"
api_key = os.environ["BETTERTOKEN_API_KEY"]
model = os.getenv("VIDEO_MODEL", "wan-2.7-t2v")
state_path = Path(os.getenv("VIDEO_STATE_FILE", "video-state.json"))
sample_image = "https://docs.bettertoken.ai/images/nano-banana-guide/sample-07.png"
body = {
"model": "wan-2.7-t2v",
"operation": "text_to_video",
"prompt": "A blue ceramic mug on a white studio table. The camera slowly moves closer. Soft daylight.",
"duration": {
"mode": "fixed",
"seconds": 5
},
"output": {
"resolution": "720p",
"aspect_ratio": "16:9",
"format": "mp4"
}
}
seedance_models = {
"seedance-2.5-cn", "seedance-2.0-cn",
"seedance-2.0-fast-cn", "seedance-2.0-mini-cn",
}
allowed_models = seedance_models | {
"kling-3.0", "minimax-h3", "wan-3.0", "wan-3.0-prime",
"wan-2.7-t2v", "wan-2.7-i2v",
}
if model not in allowed_models:
raise ValueError("Choose a Model ID from the documented batch")
body["model"] = model
if model in seedance_models or model in {"wan-3.0", "wan-3.0-prime"}:
body["audio"] = {"mode": "off"}
elif model == "kling-3.0":
body["output"].pop("resolution")
body["audio"] = {"mode": "off"}
body["provider_options"] = {"kling": {"mode": "std"}}
elif model == "minimax-h3":
body["output"]["resolution"] = "768p"
elif model == "wan-2.7-i2v":
body["operation"] = "image_to_video"
body["prompt"] = "The camera slowly moves around the blue mug. Preserve its shape and the background."
body["output"].pop("aspect_ratio")
body["media"] = [{
"id": "first-frame", "type": "image", "role": "first_frame",
"source": os.getenv("VIDEO_IMAGE_URL", sample_image),
}]
request_file = os.getenv("VIDEO_REQUEST_FILE")
if request_file:
body = json.loads(Path(request_file).read_text(encoding="utf-8"))
def api(method, path, payload=None, extra_headers=None):
headers = {"Authorization": f"Bearer {api_key}"}
headers.update(extra_headers or {})
data = None
if payload is not None:
data = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json"
request = Request(base_url + path, data=data, headers=headers, method=method)
try:
with urlopen(request, timeout=60) as response:
return json.load(response)
except HTTPError as error:
message = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"HTTP {error.code}: {message}") from error
digest = hashlib.sha256(json.dumps(body, sort_keys=True).encode("utf-8")).hexdigest()
if state_path.exists():
state = json.loads(state_path.read_text(encoding="utf-8"))
if state["request_sha256"] != digest:
raise ValueError("Request changed. Use a new VIDEO_STATE_FILE for a new task")
else:
state = {"request_sha256": digest, "idempotency_key": str(uuid.uuid4())}
state_path.write_text(json.dumps(state), encoding="utf-8")
task_id = state.get("task_id")
if not task_id:
if not state.get("create_attempted"):
catalogue = api("GET", "/video-models")
if not any(row["id"] == body["model"] and row["operation"] == body["operation"]
for row in catalogue["data"]):
raise RuntimeError("This model and operation are not in the current catalogue")
quote = api("POST", "/videos/quote", body)
print(json.dumps(quote, ensure_ascii=False, indent=2))
if input("Review the quote. Type GENERATE to create a paid task: ") != "GENERATE":
raise SystemExit("No generation request sent")
if time.time() >= quote["expires_at"]:
raise RuntimeError("Quote expired. Re-run to request a new quote")
state["create_attempted"] = True
state_path.write_text(json.dumps(state), encoding="utf-8")
task = api("POST", "/videos", body, {"Idempotency-Key": state["idempotency_key"]})
task_id = task["id"]
state["task_id"] = task_id
state_path.write_text(json.dumps(state), encoding="utf-8")
print("Task ID:", task_id)
deadline = time.monotonic() + 1800
while time.monotonic() < deadline:
task = api("GET", f"/videos/{task_id}")
print(task["status"], task["stage"], task["delivery_status"], task["financial_status"])
if task["status"] in {"failed", "canceled"}:
raise RuntimeError(json.dumps(task.get("error"), ensure_ascii=False))
if task["delivery_status"] in {"expired", "unavailable"}:
raise RuntimeError("Video download is no longer available")
if task["status"] == "completed" and task["delivery_status"] == "ready":
print(json.dumps(task["billing"], ensure_ascii=False, indent=2))
break
time.sleep(5)
else:
raise TimeoutError("Task still pending. Re-run with the same state file to resume")
request = Request(base_url + f"/videos/{task_id}/content",
headers={"Authorization": f"Bearer {api_key}"})
with urlopen(request, timeout=120) as response:
if response.headers.get_content_type() != "video/mp4":
raise RuntimeError("Expected an MP4 response; inspect the task before retrying")
with Path("video.mp4").open("wb") as output:
shutil.copyfileobj(response, output)
print("Saved video.mp4")

图生视频

将 VIDEO_MODEL 设为 wan-2.7-i2v,脚本会使用下方公开样图。也可通过 VIDEO_IMAGE_URL 换成自己有权使用、且服务端可访问的 HTTPS 图片。此入口不传 output.aspect_ratio。

图生视频示例输入:白色背景上的蓝色陶瓷杯
{
"model": "wan-2.7-i2v",
"operation": "image_to_video",
"prompt": "The camera slowly moves around the blue mug. Preserve the mug shape and the white background.",
"duration": {
"mode": "fixed",
"seconds": 5
},
"output": {
"resolution": "720p",
"format": "mp4"
},
"media": [
{
"id": "first-frame",
"type": "image",
"role": "first_frame",
"source": "https://docs.bettertoken.ai/images/nano-banana-guide/sample-07.png"
}
]
}

参考生成与自定义请求

把下方 JSON 存为 request.json,设置 VIDEO_REQUEST_FILE=request.json,并为新任务使用新的状态文件。脚本会对同一请求完成报价、确认、提交、轮询与下载。素材必须满足所选模型的类型和大小要求。

{
"model": "seedance-2.5-cn",
"operation": "reference_to_video",
"prompt": "Create a studio product video of the blue mug in the reference image. Keep the mug design consistent.",
"duration": {
"mode": "fixed",
"seconds": 5
},
"output": {
"resolution": "720p",
"aspect_ratio": "16:9",
"format": "mp4"
},
"audio": {
"mode": "off"
},
"media": [
{
"id": "reference-1",
"type": "image",
"role": "reference_image",
"source": "https://docs.bettertoken.ai/images/nano-banana-guide/sample-07.png"
}
]
}

H3 成片再生成

先将占位符替换为自己的已完成、仍可下载且满足再生成条件的 768p H3 任务 ID。这是新的付费任务,可复用上面的自定义请求流程。不要添加新的提示词、时长、输出设置或素材;服务器会沿用原任务内容并验证条件。

{
"model": "minimax-h3",
"operation": "video_edit",
"provider_options": {
"hailuo": {
"source_task_id": "YOUR_COMPLETED_H3_TASK_ID"
}
}
}

怎样判断任务完成

字段或状态处理方式
202仅表示已受理,不是成片已完成
queued / in_progress任务仍在排队或执行,继续查询
completed + delivery_status: ready成片可下载
failed / canceled停止轮询,读取错误;不要自动新建任务
delivery_status: expired / unavailable不能再按当前任务下载
financial_status金额状态与生成、下载状态分开;等待账务确认

读取报价与结算

currency 是实际报价币种,不要在生成 JSON 中传入币种。reserved_amount 是预留额度,max_charge_amount 是报价上限;提交时会重新计算并冻结任务报价。最终查看 billing.currency、billing.net_amount 与 financial_status。金额为十进制字符串;缺失或 null 不表示免费。当前报价见 查看当前模型与价格.

常见错误

现象下一步
401核对 API Key,不要把 Key 放入 JSON 或公开日志
409保留原幂等键和请求;已知任务 ID 时先查询
422按错误中的字段位置和可选值修改参数
410成片已过期;后续任务应在有效期内下载
429 / 503 / timeout先查询已有任务;提交结果不明时复用原幂等键,不自动换键重发