Видео API: расчёт, генерация, опрос и скачивание
BetterToken API Key и возобновляемый пример Python для расчёта, асинхронной генерации, проверки статуса и скачивания MP4 для десяти моделей.
Генерация видео асинхронна. Сначала проверьте возможности модели и получите расчёт. Создайте задачу, сохраните её ID и проверяйте статус. Скачивайте видео после готовности.
Подключение
| Настройка | Значение |
|---|---|
| Base URL | https://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 CN | 5 секунд, 720p, 16:9, звук выключен |
| Kling 3.0 | 5 секунд, 16:9, стандартный режим; без общего поля разрешения |
| MiniMax H3 | 5 секунд, 768p, 16:9; звук по умолчанию модели |
| Wan 3.0 / Prime | 5 секунд, 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 hashlibimport jsonimport osimport shutilimport timeimport uuidfrom pathlib import Pathfrom urllib.error import HTTPErrorfrom 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"] = modelif 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() + 1800while 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 | Сначала проверьте существующую задачу; при неопределённом создании повторите исходный ключ, не создавайте новый автоматически |

