動画 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 の結果を再生成
プレースホルダーを、自分の完了済みでダウンロード可能な 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 を確認。JSON や公開ログに含めない |
409 | 元の冪等キーとリクエストを保持。既知のタスク ID を先に確認 |
422 | エラーのフィールド位置と許可値に従って修正 |
410 | 動画の期限切れ。次回は期限内にダウンロード |
429 / 503 / timeout | 既存タスクを先に確認。作成結果が不明なら元の冪等キーを使い、新しいキーで自動再送しない |

