API de vídeo: cotação, geração, consulta e download
Use uma BetterToken API Key e um exemplo Python retomável para cotar, gerar de forma assíncrona, consultar e baixar vídeos MP4.
A geração de vídeo é assíncrona. Verifique as capacidades do modelo e solicite uma cotação antes de criar uma tarefa. Salve o ID, consulte o status e baixe quando o vídeo estiver pronto.
Conexão
| Configuração | Valor |
|---|---|
| Base URL | https://bettertoken.ai/v1 |
| Autenticação | Authorization: Bearer YOUR_API_KEY |
| Listar modelos disponíveis | GET /v1/video-models |
| Cotar antes de gerar | POST /v1/videos/quote |
| Criar uma tarefa | POST /v1/videos |
| Consultar uma tarefa | GET /v1/videos/{id} |
| Baixar o vídeo | GET /v1/videos/{id}/content |
| Criação compatível | POST /v1/video/generations |
Listar modelos disponíveis
export BETTERTOKEN_API_KEY="YOUR_API_KEY"
curl --fail-with-body "https://bettertoken.ai/v1/video-models" \ -H "Authorization: Bearer $BETTERTOKEN_API_KEY"Cada entrada do catálogo é identificada por id e operation em conjunto. Respeite seus fields, media_limits e constraints; as operações não usam todas o mesmo conjunto de parâmetros.
Configurações iniciais por família
| Família | Configurações do exemplo |
|---|---|
| Seedance CN | 5 segundos, 720p, 16:9, áudio desligado |
| Kling 3.0 | 5 segundos, 16:9, modo padrão; sem campo genérico de resolução |
| MiniMax H3 | 5 segundos, 768p, 16:9; áudio padrão do modelo |
| Wan 3.0 / Prime | 5 segundos, 720p, 16:9, áudio desligado |
| Wan 2.7 T2V / I2V | Texto: 5 segundos, 720p, 16:9; imagem: 5 segundos, 720p, proporção da entrada |
São combinações iniciais do script, não todos os limites dos modelos. Mudar de família pode exigir mais que trocar o nome. Os dez IDs exatos e as operações estão na seleção de modelos.
Fluxo completo em Python
Use Python 3 sem dependências adicionais. Defina as variáveis de ambiente, salve o código como video_example.py e execute. A cotação não inicia a geração. Digitar GENERATE cria uma tarefa real e pode gerar cobrança.
export BETTERTOKEN_API_KEY="YOUR_API_KEY"export VIDEO_MODEL="wan-2.7-t2v"export VIDEO_STATE_FILE="video-state.json"python3 video_example.pyUse um novo VIDEO_STATE_FILE para cada tarefa nova. Ao repetir a mesma tarefa, mantenha a solicitação, a BetterToken API Key e o arquivo de estado. O script reutiliza o Idempotency-Key ou o ID salvo. O limite de espera é uma configuração do cliente, não uma promessa de duração da geração.
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")Imagem para vídeo
Defina VIDEO_MODEL como wan-2.7-i2v para usar a imagem pública abaixo. Com VIDEO_IMAGE_URL, use sua própria imagem HTTPS autorizada e acessível ao servidor. Não envie output.aspect_ratio nessa entrada.

{ "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" } ]}Referências e solicitações personalizadas
Salve o JSON abaixo como request.json, defina VIDEO_REQUEST_FILE=request.json e use um novo arquivo de estado. O script cota, confirma e cria a mesma solicitação, consulta o status e baixa o resultado. As mídias devem atender aos tipos e tamanhos do modelo.
{ "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" } ]}Regenerar um resultado H3
Substitua o marcador pelo ID de sua própria tarefa H3 concluída em 768p, ainda disponível para download e apta à regeneração. É uma nova tarefa paga pelo fluxo personalizado acima. Não adicione outro prompt, duração, configurações de saída ou mídias; o servidor reutiliza a tarefa original e valida as condições.
{ "model": "minimax-h3", "operation": "video_edit", "provider_options": { "hailuo": { "source_task_id": "YOUR_COMPLETED_H3_TASK_ID" } }}Identificar a conclusão
| Campo ou status | Ação |
|---|---|
202 | Apenas aceita; o vídeo ainda não está pronto |
queued / in_progress | Na fila ou em execução; continue consultando |
completed + delivery_status: ready | Pronto para download |
failed / canceled | Pare a consulta e leia o erro; não crie outra tarefa automaticamente |
delivery_status: expired / unavailable | O vídeo desta tarefa não pode mais ser baixado |
financial_status | A cobrança é independente da execução e do download; aguarde confirmação |
Ler a cotação e a liquidação
currency informa a moeda da cotação; não a envie no JSON de geração. reserved_amount é a reserva e max_charge_amount o limite estimado. A criação recalcula e fixa a cotação da tarefa. Leia billing.currency, billing.net_amount e financial_status para a liquidação. Os valores são strings decimais; ausência ou null não significa gratuito. Consulte os preços atuais no Modelos e preços atuais.
Erros comuns
| Situação | Próxima etapa |
|---|---|
401 | Verifique a API Key; não a coloque no JSON nem em logs públicos |
409 | Mantenha a chave de idempotência e a solicitação originais; consulte primeiro o ID conhecido |
422 | Corrija o campo conforme o caminho e os valores permitidos do erro |
410 | O vídeo expirou; baixe os próximos resultados antes do vencimento |
429 / 503 / timeout | Consulte a tarefa existente; se a criação for incerta, reutilize a chave original sem enviar outra automaticamente |

