API de vídeo: presupuesto, generación, consulta y descarga
Usa una BetterToken API Key y un ejemplo Python reanudable para obtener presupuestos, generar de forma asíncrona, consultar y descargar MP4.
La generación de vídeo es asíncrona. Comprueba las capacidades y solicita un presupuesto antes de crear una tarea. Guarda su ID, consulta el estado y descarga cuando el vídeo esté listo.
Conexión
| Ajuste | Valor |
|---|---|
| Base URL | https://bettertoken.ai/v1 |
| Autenticación | Authorization: Bearer YOUR_API_KEY |
| Listar modelos disponibles | GET /v1/video-models |
| Presupuesto antes de generar | POST /v1/videos/quote |
| Crear una tarea | POST /v1/videos |
| Consultar una tarea | GET /v1/videos/{id} |
| Descargar el vídeo | GET /v1/videos/{id}/content |
| Creación compatible | POST /v1/video/generations |
Listar modelos disponibles
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 del catálogo se identifica por id y operation juntos. Respeta sus fields, media_limits y constraints; no uses los mismos parámetros para todas las operaciones.
Ajustes iniciales por familia
| Familia | Ajustes del ejemplo |
|---|---|
| Seedance CN | 5 segundos, 720p, 16:9, sin audio |
| Kling 3.0 | 5 segundos, 16:9, modo estándar; sin campo genérico de resolución |
| MiniMax H3 | 5 segundos, 768p, 16:9; audio predeterminado del modelo |
| Wan 3.0 / Prime | 5 segundos, 720p, 16:9, sin audio |
| Wan 2.7 T2V / I2V | Texto: 5 segundos, 720p, 16:9; imagen: 5 segundos, 720p, proporción de la entrada |
Son combinaciones iniciales del script, no todos los límites de cada modelo. Cambiar de familia puede exigir más que cambiar el nombre. Los diez ID exactos y sus operaciones están en la selección de modelos.
Flujo completo en Python
Usa Python 3 sin dependencias adicionales. Define las variables de entorno, guarda el código como video_example.py y ejecútalo. El presupuesto no inicia la generación. Escribir GENERATE crea una tarea real y puede generar cargos.
export BETTERTOKEN_API_KEY="YOUR_API_KEY"export VIDEO_MODEL="wan-2.7-t2v"export VIDEO_STATE_FILE="video-state.json"python3 video_example.pyUsa un nuevo VIDEO_STATE_FILE para cada tarea nueva. Para reintentar la misma tarea, conserva la solicitud, la BetterToken API Key y el archivo de estado. El script reutiliza el Idempotency-Key o el ID guardado. El tiempo máximo de espera es una configuración del cliente, no una promesa de generación.
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")De imagen a vídeo
Define VIDEO_MODEL como wan-2.7-i2v para usar la imagen pública siguiente. Usa VIDEO_IMAGE_URL para indicar tu propia imagen HTTPS autorizada y accesible al servidor. No envíes output.aspect_ratio en esta 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" } ]}Referencias y solicitudes personalizadas
Guarda el JSON siguiente como request.json, define VIDEO_REQUEST_FILE=request.json y usa un nuevo archivo de estado. El script presupuesta, confirma y crea la misma solicitud, consulta su estado y descarga el resultado. Los medios deben cumplir los tipos y tamaños del 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 un resultado H3
Sustituye el marcador por el ID de tu propia tarea H3 terminada en 768p, aún descargable y apta para regeneración. Es una tarea nueva de pago mediante el flujo personalizado anterior. No añadas otro prompt, duración, ajustes de salida ni medios; el servidor reutiliza la tarea original y valida las condiciones.
{ "model": "minimax-h3", "operation": "video_edit", "provider_options": { "hailuo": { "source_task_id": "YOUR_COMPLETED_H3_TASK_ID" } }}Reconocer la finalización
| Campo o estado | Acción |
|---|---|
202 | Solo aceptada; el vídeo aún no está terminado |
queued / in_progress | En cola o en ejecución; sigue consultando |
completed + delivery_status: ready | Lista para descargar |
failed / canceled | Detén la consulta y lee el error; no crees otra tarea automáticamente |
delivery_status: expired / unavailable | Ya no se puede descargar el vídeo de esta tarea |
financial_status | La facturación es independiente de la generación y la descarga; espera confirmación |
Leer el presupuesto y la liquidación
currency indica la moneda del presupuesto; no la envíes en el JSON de generación. reserved_amount es la reserva y max_charge_amount el límite estimado. Al crear la tarea se recalcula y fija su presupuesto. Consulta billing.currency, billing.net_amount y financial_status para la liquidación. Los importes son cadenas decimales; una cifra ausente o null no significa gratis. Consulta los precios actuales en el Modelos y precios actuales.
Errores frecuentes
| Situación | Siguiente paso |
|---|---|
401 | Comprueba la API Key; no la pongas en JSON ni registros públicos |
409 | Conserva la clave de idempotencia y la solicitud originales; consulta primero el ID conocido |
422 | Corrige el campo según la ruta y los valores permitidos del error |
410 | El vídeo ha caducado; descarga los próximos resultados antes de que caduquen |
429 / 503 / timeout | Consulta la tarea existente; ante una creación incierta, reutiliza la clave original sin crear otra automáticamente |

