Aller au contenu
Inscrivez-vous gratuitement
Référence de l'API

API vidéo : devis, génération, suivi et téléchargement

Utilisez une BetterToken API Key et un exemple Python reprenable pour obtenir un devis, générer en différé, suivre la tâche et télécharger un MP4.

La génération vidéo est asynchrone. Vérifiez les capacités du modèle et demandez un devis avant de créer une tâche. Conservez son ID, suivez son état et téléchargez la vidéo lorsqu’elle est prête.

Connexion

ParamètreValeur
Base URLhttps://bettertoken.ai/v1
AuthentificationAuthorization: Bearer YOUR_API_KEY
Lister les modèles disponiblesGET /v1/video-models
Devis avant générationPOST /v1/videos/quote
Créer une tâchePOST /v1/videos
Consulter une tâcheGET /v1/videos/{id}
Télécharger la vidéoGET /v1/videos/{id}/content
Création compatiblePOST /v1/video/generations

Lister les modèles disponibles

Fenêtre de terminal
export BETTERTOKEN_API_KEY="YOUR_API_KEY"
curl --fail-with-body "https://bettertoken.ai/v1/video-models" \
-H "Authorization: Bearer $BETTERTOKEN_API_KEY"

Chaque entrée du catalogue est identifiée par id et operation ensemble. Respectez ses fields, media_limits et constraints ; les opérations n’utilisent pas toutes les mêmes paramètres.

Réglages de départ par famille

FamilleRéglages de l’exemple
Seedance CN5 secondes, 720p, 16:9, audio désactivé
Kling 3.05 secondes, 16:9, mode standard ; pas de champ de résolution générique
MiniMax H35 secondes, 768p, 16:9 ; audio par défaut du modèle
Wan 3.0 / Prime5 secondes, 720p, 16:9, audio désactivé
Wan 2.7 T2V / I2VTexte : 5 secondes, 720p, 16:9 ; image : 5 secondes, 720p, ratio de la source

Ce sont les réglages de départ du script, pas les limites complètes des modèles. Changer de famille peut nécessiter plus qu’un autre nom de modèle. Les dix ID exacts et les opérations figurent dans la sélection de modèles.

Flux Python complet

Utilisez Python 3 sans dépendance supplémentaire. Définissez les variables d’environnement, enregistrez le code dans video_example.py et exécutez-le. Le devis ne lance pas de génération. Saisir GENERATE crée une vraie tâche et peut entraîner des frais.

Fenêtre de terminal
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

Utilisez un nouveau VIDEO_STATE_FILE pour une nouvelle tâche. Pour réessayer la même tâche, conservez la requête, la BetterToken API Key et le fichier d’état. Le script réutilise l’Idempotency-Key ou l’ID enregistré. Le délai d’attente est un réglage client, pas une promesse de durée de génération.

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")

Image vers vidéo

Définissez VIDEO_MODEL sur wan-2.7-i2v pour utiliser l’image publique ci-dessous. Avec VIDEO_IMAGE_URL, choisissez votre propre image HTTPS autorisée et accessible au serveur. Ne transmettez pas output.aspect_ratio à cette entrée.

Image source : tasse en céramique bleue sur fond blanc
{
"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"
}
]
}

Références et requêtes personnalisées

Enregistrez le JSON suivant dans request.json, définissez VIDEO_REQUEST_FILE=request.json et utilisez un nouveau fichier d’état. Le script chiffre, confirme et crée cette requête, puis suit et télécharge le résultat. Les médias doivent respecter les types et tailles du modèle choisi.

{
"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"
}
]
}

Régénérer un résultat H3

Remplacez la valeur d’exemple par l’ID de votre propre tâche H3 terminée en 768p, encore téléchargeable et admissible à la régénération. Il s’agit d’une nouvelle tâche payante via le flux personnalisé ci-dessus. N’ajoutez ni prompt, durée, réglages de sortie ni médias : le serveur reprend la tâche d’origine et vérifie les conditions.

{
"model": "minimax-h3",
"operation": "video_edit",
"provider_options": {
"hailuo": {
"source_task_id": "YOUR_COMPLETED_H3_TASK_ID"
}
}
}

Reconnaître la fin d’une tâche

Champ ou étatAction
202Acceptée seulement ; la vidéo n’est pas terminée
queued / in_progressEn attente ou en cours ; continuez le suivi
completed + delivery_status: readyPrête au téléchargement
failed / canceledArrêtez le suivi et lisez l’erreur ; ne créez pas automatiquement une autre tâche
delivery_status: expired / unavailableLa vidéo ne peut plus être téléchargée pour cette tâche
financial_statusLa facturation est distincte de la génération et du téléchargement ; attendez sa confirmation

Lire le devis et le règlement

currency indique la devise du devis ; ne la transmettez pas dans le JSON de génération. reserved_amount est la réserve et max_charge_amount le plafond estimé. La création recalcule et fige le devis de la tâche. Consultez billing.currency, billing.net_amount et financial_status pour le règlement. Les montants sont des chaînes décimales ; une valeur absente ou null ne signifie pas gratuit. Les prix actuels figurent dans le Modèles et prix actuels.

Erreurs courantes

SituationÉtape suivante
401Vérifiez l’API Key ; ne la placez pas dans le JSON ou des journaux publics
409Conservez la clé d’idempotence et la requête d’origine ; consultez d’abord l’ID connu
422Corrigez le champ selon son chemin et les valeurs autorisées de l’erreur
410La vidéo a expiré ; téléchargez les prochains résultats avant expiration
429 / 503 / timeoutConsultez la tâche existante ; après une création incertaine, réutilisez la clé d’origine sans en envoyer automatiquement une nouvelle