Перейти к содержимому
Зарегистрироваться бесплатно
API Reference

Видео 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

Замените заполнитель ID собственной завершённой задачи H3 в 768p, которую ещё можно скачать и которая отвечает условиям повторной генерации. Это новая платная задача для описанного выше потока собственных запросов. Не добавляйте новый текст, длительность, настройки вывода или материалы: сервер использует исходную задачу и проверяет условия.

{
"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; не помещайте его в JSON или открытые логи
409Сохраните исходные ключ идемпотентности и запрос; сначала проверьте известный ID задачи
422Исправьте поле по пути и допустимым значениям из ошибки
410Срок хранения истёк; в следующих задачах скачивайте результат вовремя
429 / 503 / timeoutСначала проверьте существующую задачу; при неопределённом создании повторите исходный ключ, не создавайте новый автоматически