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ètre | Valeur |
|---|---|
| Base URL | https://bettertoken.ai/v1 |
| Authentification | Authorization: Bearer YOUR_API_KEY |
| Lister les modèles disponibles | GET /v1/video-models |
| Devis avant génération | POST /v1/videos/quote |
| Créer une tâche | POST /v1/videos |
| Consulter une tâche | GET /v1/videos/{id} |
| Télécharger la vidéo | GET /v1/videos/{id}/content |
| Création compatible | POST /v1/video/generations |
Lister les modèles disponibles
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
| Famille | Réglages de l’exemple |
|---|---|
| Seedance CN | 5 secondes, 720p, 16:9, audio désactivé |
| Kling 3.0 | 5 secondes, 16:9, mode standard ; pas de champ de résolution générique |
| MiniMax H3 | 5 secondes, 768p, 16:9 ; audio par défaut du modèle |
| Wan 3.0 / Prime | 5 secondes, 720p, 16:9, audio désactivé |
| Wan 2.7 T2V / I2V | Texte : 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.
export BETTERTOKEN_API_KEY="YOUR_API_KEY"export VIDEO_MODEL="wan-2.7-t2v"export VIDEO_STATE_FILE="video-state.json"python3 video_example.pyUtilisez 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 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")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.

{ "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 état | Action |
|---|---|
202 | Acceptée seulement ; la vidéo n’est pas terminée |
queued / in_progress | En attente ou en cours ; continuez le suivi |
completed + delivery_status: ready | Prête au téléchargement |
failed / canceled | Arrêtez le suivi et lisez l’erreur ; ne créez pas automatiquement une autre tâche |
delivery_status: expired / unavailable | La vidéo ne peut plus être téléchargée pour cette tâche |
financial_status | La 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 |
|---|---|
401 | Vérifiez l’API Key ; ne la placez pas dans le JSON ou des journaux publics |
409 | Conservez la clé d’idempotence et la requête d’origine ; consultez d’abord l’ID connu |
422 | Corrigez le champ selon son chemin et les valeurs autorisées de l’erreur |
410 | La vidéo a expiré ; téléchargez les prochains résultats avant expiration |
429 / 503 / timeout | Consultez la tâche existante ; après une création incertaine, réutilisez la clé d’origine sans en envoyer automatiquement une nouvelle |

