동영상 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 10개와 작업은 다음을 참고하세요: 모델 모음.
전체 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 | 기존 작업부터 조회. 생성 결과가 불명확하면 원래 멱등 키를 재사용하고 새 키로 자동 재전송하지 않음 |

