# WAN 3.0 ## alibaba/wan-3.0/image-to-video Category: image-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/alibaba/wan-3.0/image-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "input": { "prompt": "A blue paper boat floating on a calm pond", "media": [ { "type": "first_frame", "url": "https://example.com/reference.png" } ] }, "parameters": { "duration": 5, "resolution": "720P", "ratio": "16:9", "audio": true } }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/alibaba/wan-3.0/image-to-video) · [Full documentation](/docs/models/alibaba/wan-3.0) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/alibaba/wan-3.0/image-to-video/predictions" payload = json.loads("{\n \"input\": {\n \"prompt\": \"A blue paper boat floating on a calm pond\",\n \"media\": [\n {\n \"type\": \"first_frame\",\n \"url\": \"https://example.com/reference.png\"\n }\n ]\n },\n \"parameters\": {\n \"duration\": 5,\n \"resolution\": \"720P\",\n \"ratio\": \"16:9\",\n \"audio\": true\n }\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/alibaba/wan-3.0/image-to-video/predictions"; const input = { "input": { "prompt": "A blue paper boat floating on a calm pond", "media": [ { "type": "first_frame", "url": "https://example.com/reference.png" } ] }, "parameters": { "duration": 5, "resolution": "720P", "ratio": "16:9", "audio": true } }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | input.prompt | Yes | string | — | min length 1; max length 10000 | Describe the desired content. | | first_frame | input.media[].url | Yes | string | — | min length 1 | Official: 240–8000 px per side; longest/shortest side at most 8; at most 20 MB. PNG alpha channels are not supported. Platform input: PNG/JPEG/WebP only, at most 20 MB (20,000,000 bytes), at most 40 megapixels. Checks run before a quote is created. media item: type=first_frame. | | last_frame | input.media[].url | No | string | — | — | Official: 240–8000 px per side; longest/shortest side at most 8; at most 20 MB. PNG alpha channels are not supported. Platform input: PNG/JPEG/WebP only, at most 20 MB (20,000,000 bytes), at most 40 megapixels. Checks run before a quote is created. media item: type=last_frame. | | aspect_ratio | parameters.ratio | No | string | Form: "adaptive"; API: — | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | parameters.resolution | No | string | Form: "720P"; API: — | "480P", "720P", "1080P" | Output resolution. | | duration | parameters.duration | No | integer | Form: 5; API: — | -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 | Output duration in seconds. 2–30 seconds or -1 for automatic duration. | | generate_audio | parameters.audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | input | input | No | object | — | — | Model input object. | | input.prompt | input.prompt | No | string | — | max length 20000 | Describe the desired content. | | input.media | input.media | No | object[] | — | — | Image inputs: Official: 240–8000 px per side; longest/shortest side at most 8; at most 20 MB. PNG alpha channels are not supported. Platform input: PNG/JPEG/WebP only, at most 20 MB (20,000,000 bytes), at most 40 megapixels. Checks run before a quote is created. | | input.media[].type | input.media[].type | When parent is supplied | string | — | "first_frame", "last_frame", "reference_image", "reference_video", "reference_audio", "file", "link" | Content or media item type. | | input.media[].url | input.media[].url | When parent is supplied | string | — | — | Public media URL. | | parameters | parameters | No | object | — | — | Generation settings. | | parameters.resolution | parameters.resolution | No | string | — | "480P", "720P", "1080P" | Output resolution. | | parameters.ratio | parameters.ratio | No | string | — | — | Output aspect ratio. | | parameters.duration | parameters.duration | No | integer | — | — | 2–30 seconds or -1 for automatic duration. | | parameters.audio | parameters.audio | No | boolean | — | true, false | Input audio. | | parameters.seed | parameters.seed | No | integer | — | — | Random seed. | | parameters.prompt_extend | parameters.prompt_extend | No | boolean | — | true, false | Whether to expand the prompt. | | parameters.watermark | parameters.watermark | No | boolean | — | true, false | Whether to add a watermark. | ```json { "type": "object", "properties": { "input": { "type": "object", "properties": { "prompt": { "type": "string", "maxLength": 20000 }, "media": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "first_frame", "last_frame", "reference_image", "reference_video", "reference_audio", "file", "link" ] }, "url": { "type": "string" } }, "required": [ "type", "url" ] }, "description": " Image inputs: Official: 240–8000 px per side; longest/shortest side at most 8; at most 20 MB. PNG alpha channels are not supported. Platform input: PNG/JPEG/WebP only, at most 20 MB (20,000,000 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "parameters": { "type": "object", "properties": { "resolution": { "type": "string", "enum": [ "480P", "720P", "1080P" ] }, "ratio": { "type": "string" }, "duration": { "type": "integer", "description": "2–30 seconds or -1 for automatic duration." }, "audio": { "type": "boolean" }, "seed": { "type": "integer" }, "prompt_extend": { "type": "boolean" }, "watermark": { "type": "boolean" } } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged." } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## alibaba/wan-3.0/text-to-video Category: text-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/alibaba/wan-3.0/text-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "input": { "prompt": "A blue paper boat floating on a calm pond" }, "parameters": { "duration": 5, "resolution": "720P", "ratio": "16:9", "audio": true } }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/alibaba/wan-3.0/text-to-video) · [Full documentation](/docs/models/alibaba/wan-3.0) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/alibaba/wan-3.0/text-to-video/predictions" payload = json.loads("{\n \"input\": {\n \"prompt\": \"A blue paper boat floating on a calm pond\"\n },\n \"parameters\": {\n \"duration\": 5,\n \"resolution\": \"720P\",\n \"ratio\": \"16:9\",\n \"audio\": true\n }\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/alibaba/wan-3.0/text-to-video/predictions"; const input = { "input": { "prompt": "A blue paper boat floating on a calm pond" }, "parameters": { "duration": 5, "resolution": "720P", "ratio": "16:9", "audio": true } }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | input.prompt | Yes | string | — | min length 1; max length 10000 | Describe the desired content. | | aspect_ratio | parameters.ratio | No | string | Form: "16:9"; API: — | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | parameters.resolution | No | string | Form: "720P"; API: — | "480P", "720P", "1080P" | Output resolution. | | duration | parameters.duration | No | integer | Form: 5; API: — | -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 | Output duration in seconds. 2–30 seconds or -1 for automatic duration. | | generate_audio | parameters.audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | input | input | No | object | — | — | Model input object. | | input.prompt | input.prompt | No | string | — | max length 20000 | Describe the desired content. | | input.media | input.media | No | object[] | — | — | Image inputs: Official: 240–8000 px per side; longest/shortest side at most 8; at most 20 MB. PNG alpha channels are not supported. Platform input: PNG/JPEG/WebP only, at most 20 MB (20,000,000 bytes), at most 40 megapixels. Checks run before a quote is created. | | input.media[].type | input.media[].type | When parent is supplied | string | — | "first_frame", "last_frame", "reference_image", "reference_video", "reference_audio", "file", "link" | Content or media item type. | | input.media[].url | input.media[].url | When parent is supplied | string | — | — | Public media URL. | | parameters | parameters | No | object | — | — | Generation settings. | | parameters.resolution | parameters.resolution | No | string | — | "480P", "720P", "1080P" | Output resolution. | | parameters.ratio | parameters.ratio | No | string | — | — | Output aspect ratio. | | parameters.duration | parameters.duration | No | integer | — | — | 2–30 seconds or -1 for automatic duration. | | parameters.audio | parameters.audio | No | boolean | — | true, false | Input audio. | | parameters.seed | parameters.seed | No | integer | — | — | Random seed. | | parameters.prompt_extend | parameters.prompt_extend | No | boolean | — | true, false | Whether to expand the prompt. | | parameters.watermark | parameters.watermark | No | boolean | — | true, false | Whether to add a watermark. | ```json { "type": "object", "properties": { "input": { "type": "object", "properties": { "prompt": { "type": "string", "maxLength": 20000 }, "media": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "first_frame", "last_frame", "reference_image", "reference_video", "reference_audio", "file", "link" ] }, "url": { "type": "string" } }, "required": [ "type", "url" ] }, "description": " Image inputs: Official: 240–8000 px per side; longest/shortest side at most 8; at most 20 MB. PNG alpha channels are not supported. Platform input: PNG/JPEG/WebP only, at most 20 MB (20,000,000 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "parameters": { "type": "object", "properties": { "resolution": { "type": "string", "enum": [ "480P", "720P", "1080P" ] }, "ratio": { "type": "string" }, "duration": { "type": "integer", "description": "2–30 seconds or -1 for automatic duration." }, "audio": { "type": "boolean" }, "seed": { "type": "integer" }, "prompt_extend": { "type": "boolean" }, "watermark": { "type": "boolean" } } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged." } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## alibaba/wan-3.0/reference-to-video Category: Reference to Video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/alibaba/wan-3.0/reference-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "input": { "prompt": "A blue paper boat floating on a calm pond", "media": [ { "type": "reference_image", "url": "https://example.com/reference.png" } ] }, "parameters": { "duration": 5, "resolution": "720P", "ratio": "16:9", "audio": true } }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/alibaba/wan-3.0/reference-to-video) · [Full documentation](/docs/models/alibaba/wan-3.0) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/alibaba/wan-3.0/reference-to-video/predictions" payload = json.loads("{\n \"input\": {\n \"prompt\": \"A blue paper boat floating on a calm pond\",\n \"media\": [\n {\n \"type\": \"reference_image\",\n \"url\": \"https://example.com/reference.png\"\n }\n ]\n },\n \"parameters\": {\n \"duration\": 5,\n \"resolution\": \"720P\",\n \"ratio\": \"16:9\",\n \"audio\": true\n }\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/alibaba/wan-3.0/reference-to-video/predictions"; const input = { "input": { "prompt": "A blue paper boat floating on a calm pond", "media": [ { "type": "reference_image", "url": "https://example.com/reference.png" } ] }, "parameters": { "duration": 5, "resolution": "720P", "ratio": "16:9", "audio": true } }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | input.prompt | Yes | string | — | min length 1; max length 10000 | Describe the desired content. | | reference_videos | input.media[].url | No | string[] | [] | max items 5 | Reference video URLs. media item: type=reference_video. | | reference_images | input.media[].url | No | string[] | [] | max items 10 | Official: 240–8000 px per side; longest/shortest side at most 8; at most 20 MB. PNG alpha channels are not supported. Platform input: PNG/JPEG/WebP only, at most 20 MB (20,000,000 bytes), at most 40 megapixels. Checks run before a quote is created. media item: type=reference_image. | | reference_audios | input.media[].url | No | string[] | [] | max items 5 | Reference audio URLs. media item: type=reference_audio. | | aspect_ratio | parameters.ratio | No | string | Form: "16:9"; API: — | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | parameters.resolution | No | string | Form: "720P"; API: — | "480P", "720P", "1080P" | Output resolution. | | duration | parameters.duration | No | integer | Form: 5; API: — | -1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 | Output duration in seconds. 2–30 seconds or -1 for automatic duration. | | generate_audio | parameters.audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | input | input | No | object | — | — | Model input object. | | input.prompt | input.prompt | No | string | — | max length 20000 | Describe the desired content. | | input.media | input.media | No | object[] | — | — | Image inputs: Official: 240–8000 px per side; longest/shortest side at most 8; at most 20 MB. PNG alpha channels are not supported. Platform input: PNG/JPEG/WebP only, at most 20 MB (20,000,000 bytes), at most 40 megapixels. Checks run before a quote is created. | | input.media[].type | input.media[].type | When parent is supplied | string | — | "first_frame", "last_frame", "reference_image", "reference_video", "reference_audio", "file", "link" | Content or media item type. | | input.media[].url | input.media[].url | When parent is supplied | string | — | — | Public media URL. | | parameters | parameters | No | object | — | — | Generation settings. | | parameters.resolution | parameters.resolution | No | string | — | "480P", "720P", "1080P" | Output resolution. | | parameters.ratio | parameters.ratio | No | string | — | — | Output aspect ratio. | | parameters.duration | parameters.duration | No | integer | — | — | 2–30 seconds or -1 for automatic duration. | | parameters.audio | parameters.audio | No | boolean | — | true, false | Input audio. | | parameters.seed | parameters.seed | No | integer | — | — | Random seed. | | parameters.prompt_extend | parameters.prompt_extend | No | boolean | — | true, false | Whether to expand the prompt. | | parameters.watermark | parameters.watermark | No | boolean | — | true, false | Whether to add a watermark. | ```json { "type": "object", "properties": { "input": { "type": "object", "properties": { "prompt": { "type": "string", "maxLength": 20000 }, "media": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "first_frame", "last_frame", "reference_image", "reference_video", "reference_audio", "file", "link" ] }, "url": { "type": "string" } }, "required": [ "type", "url" ] }, "description": " Image inputs: Official: 240–8000 px per side; longest/shortest side at most 8; at most 20 MB. PNG alpha channels are not supported. Platform input: PNG/JPEG/WebP only, at most 20 MB (20,000,000 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "parameters": { "type": "object", "properties": { "resolution": { "type": "string", "enum": [ "480P", "720P", "1080P" ] }, "ratio": { "type": "string" }, "duration": { "type": "integer", "description": "2–30 seconds or -1 for automatic duration." }, "audio": { "type": "boolean" }, "seed": { "type": "integer" }, "prompt_extend": { "type": "boolean" }, "watermark": { "type": "boolean" } } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged." } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- # Seedance 2.5 ## bytedance/seedance-2.5/image-to-video Category: image-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.5/image-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.5/image-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.5) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.5/image-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"image_url\",\n \"role\": \"first_frame\",\n \"image_url\": {\n \"url\": \"https://example.com/reference.png\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"adaptive\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.5/image-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | first_frame | content[].image_url.url | Yes | string | — | min length 1 | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=first_frame. | | last_frame | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=last_frame. | | aspect_ratio | ratio | No | string | "adaptive" | "adaptive" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p", "1080p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 | Output duration in seconds. 4–30 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–30 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p", "1080p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## bytedance/seedance-2.5/text-to-video Category: text-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.5/text-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.5/text-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.5) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.5/text-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.5/text-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p", "1080p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 | Output duration in seconds. 4–30 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–30 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p", "1080p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## bytedance/seedance-2.5/omni-reference-to-video Category: Omni Reference to Video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.5/omni-reference-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "video_url", "role": "reference_video", "video_url": { "url": "https://example.com/reference.mp4" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.5/omni-reference-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.5) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.5/omni-reference-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"video_url\",\n \"role\": \"reference_video\",\n \"video_url\": {\n \"url\": \"https://example.com/reference.mp4\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.5/omni-reference-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "video_url", "role": "reference_video", "video_url": { "url": "https://example.com/reference.mp4" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | reference_videos | content[].video_url.url | Yes | string[] | [] | min items 1; max items 10 | Reference video URLs. content item: type=video_url, role=reference_video. | | reference_images | content[].image_url.url | No | string[] | [] | max items 30 | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=reference_image. | | reference_audios | content[].audio_url.url | No | string[] | [] | max items 10 | Reference audio URLs. content item: type=audio_url, role=reference_audio. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p", "1080p" | Output resolution. | | duration | duration | No | integer | Form: -1; API: 5 | -1 | Output duration in seconds. 4–30 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–30 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p", "1080p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- # Seedance 2.0 Mini ## bytedance/seedance-2.0-mini/image-to-video Category: image-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-mini/image-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.0-mini/image-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.0-mini) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-mini/image-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"image_url\",\n \"role\": \"first_frame\",\n \"image_url\": {\n \"url\": \"https://example.com/reference.png\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"adaptive\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-mini/image-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | first_frame | content[].image_url.url | Yes | string | — | min length 1 | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=first_frame. | | last_frame | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=last_frame. | | aspect_ratio | ratio | No | string | "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | Output duration in seconds. 4–15 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–15 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## bytedance/seedance-2.0-mini/text-to-video Category: text-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-mini/text-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.0-mini/text-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.0-mini) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-mini/text-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-mini/text-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | Output duration in seconds. 4–15 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–15 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## bytedance/seedance-2.0-mini/omni-reference-to-video Category: Omni Reference to Video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-mini/omni-reference-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "video_url", "role": "reference_video", "video_url": { "url": "https://example.com/reference.mp4" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.0-mini/omni-reference-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.0-mini) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-mini/omni-reference-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"video_url\",\n \"role\": \"reference_video\",\n \"video_url\": {\n \"url\": \"https://example.com/reference.mp4\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-mini/omni-reference-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "video_url", "role": "reference_video", "video_url": { "url": "https://example.com/reference.mp4" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | reference_videos | content[].video_url.url | No | string[] | [] | max items 3 | Reference video URLs. content item: type=video_url, role=reference_video. | | reference_images | content[].image_url.url | No | string[] | [] | max items 9 | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=reference_image. | | reference_audios | content[].audio_url.url | No | string[] | [] | max items 3 | Reference audio URLs. content item: type=audio_url, role=reference_audio. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | Output duration in seconds. 4–15 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Up to 9 reference images, 3 reference videos and 3 reference audios. Include at least one reference image or video; do not mix reference roles with first/last frames. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Up to 9 reference images, 3 reference videos and 3 reference audios. Include at least one reference image or video; do not mix reference roles with first/last frames.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–15 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- # GPT Image 2 ## openai/gpt-image-2/text-to-image Category: Text to Image Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/openai/gpt-image-2/text-to-image/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "prompt": "A blue ceramic cup on a wooden table", "resolution": "1k", "size": "1:1", "n": 1 }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/openai/gpt-image-2/text-to-image) · [Full documentation](/docs/models/openai/gpt-image-2) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/openai/gpt-image-2/text-to-image/predictions" payload = json.loads("{\n \"prompt\": \"A blue ceramic cup on a wooden table\",\n \"resolution\": \"1k\",\n \"size\": \"1:1\",\n \"n\": 1\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/openai/gpt-image-2/text-to-image/predictions"; const input = { "prompt": "A blue ceramic cup on a wooden table", "resolution": "1k", "size": "1:1", "n": 1 }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | prompt | Yes | string | — | min length 1 | Describe the desired content. | | resolution | resolution | No | string | — | "1k", "2k", "4k" | Output resolution. | | quality | quality | No | string | Form: "medium"; API: — | "auto", "low", "medium", "high" | Output quality. APIMart Official channel. auto is forwarded unchanged and uses the published high quality price tier. | | aspect_ratio | aspect_ratio | No | string | "1:1" | "1:1", "3:2", "2:3", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "9:21", "2:1", "1:2", "3:1", "1:3", "auto" | Output aspect ratio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | n | n | No | integer | 1 | min 1; max 4 | Official: 1–4. Standard: 1. Each output is billed. | | mask_url | mask_url | No | string | — | uri | Mask: PNG under 4 MB with the same dimensions as the first image. The server checks matching dimensions before quoting. | | output_format | output_format | No | string | Form: "png"; API: — | "png", "jpeg", "webp" | Output file format. Official channel. | | size | size | No | string | — | — | Aspect ratio, auto, or WxH pixel dimensions; alias of aspect_ratio. Pixel dimensions use the published 4k price tier regardless of the resolution hint. | | image_urls | image_urls | No | string[] | — | max items 16 | Official: PNG/JPEG/WebP, each image under 50 MB. No additional input pixel minimum is specified. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | output_compression | output_compression | No | integer | — | min 0; max 100 | Official channel JPEG/WebP compression. | | nsfw_check | nsfw_check | No | boolean | — | true, false | Content-safety check setting. | | moderation | moderation | No | string | — | "auto", "low" | Official channel. | ```json { "type": "object", "properties": { "prompt": { "type": "string", "minLength": 1 }, "size": { "type": "string", "description": "Aspect ratio, auto, or WxH pixel dimensions; alias of aspect_ratio. Pixel dimensions use the published 4k price tier regardless of the resolution hint." }, "resolution": { "type": "string", "enum": [ "1k", "2k", "4k" ] }, "quality": { "type": "string", "enum": [ "auto", "low", "medium", "high" ], "description": "APIMart Official channel. auto is forwarded unchanged and uses the published high quality price tier." }, "n": { "type": "integer", "minimum": 1, "maximum": 4, "default": 1, "description": "Official: 1–4. Standard: 1. Each output is billed." }, "image_urls": { "type": "array", "items": { "type": "string", "x-image-rules": { "minSide": 1, "maxSide": 16384, "maxRatio": 0, "maxBytes": 20971520, "maxPixels": 40000000, "formats": [ "image/png", "image/jpeg", "image/webp" ], "rejectPngAlpha": false, "description": "Official: PNG/JPEG/WebP, each image under 50 MB. No additional input pixel minimum is specified. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created.", "source": "https://developers.openai.com/api/reference/python/resources/images/methods/edit" } }, "maxItems": 16, "description": "Official: PNG/JPEG/WebP, each image under 50 MB. No additional input pixel minimum is specified. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." }, "mask_url": { "type": "string", "format": "uri", "description": "Mask: PNG under 4 MB with the same dimensions as the first image. The server checks matching dimensions before quoting.", "x-image-rules": { "minSide": 1, "maxSide": 16384, "maxRatio": 0, "maxBytes": 3999999, "maxPixels": 40000000, "formats": [ "image/png" ], "rejectPngAlpha": false, "description": "Mask: PNG under 4 MB with the same dimensions as the first image. The server checks matching dimensions before quoting.", "source": "https://developers.openai.com/api/reference/python/resources/images/methods/edit" } }, "output_format": { "type": "string", "enum": [ "png", "jpeg", "webp" ], "description": "Official channel." }, "output_compression": { "type": "integer", "minimum": 0, "maximum": 100, "description": "Official channel JPEG/WebP compression." }, "nsfw_check": { "type": "boolean" }, "moderation": { "type": "string", "enum": [ "auto", "low" ], "description": "Official channel." } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged. Platform pricing: pixel sizes use the 4k SKU; quality=auto uses the high SKU while forwarding auto unchanged. URL references are forwarded directly. Inline references retain the Platform 6 MB aggregate request limit. The selected supplier is pinned; automatic Official fallback is disabled.", "x-supplier-notes": [ { "supplier": "apimart", "unsupported": [ "quality", "mask_url", "moderation", "output_compression", "n > 1", "image_urls > 15", "output_format other than png" ], "reason": "These controls are provided only by the APIMart Official image endpoint." }, { "supplier": "apimart_official", "unsupported": [ "n > 4", "image_urls > 16" ], "reason": "Published APIMart Official limits." }, { "supplier": "apimart / apimart_official", "unsupported": [ "stream=true", "response_format=b64_json", "partial_images" ], "reason": "These supplier endpoints return an asynchronous task ID and URL results; no streaming or base64 response contract is published." } ], "required": [ "prompt" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## openai/gpt-image-2/edit Category: Image Editing Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/openai/gpt-image-2/edit/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "prompt": "A blue ceramic cup on a wooden table", "resolution": "1k", "size": "1:1", "n": 1, "image_urls": [ "https://example.com/reference.png" ] }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/openai/gpt-image-2/edit) · [Full documentation](/docs/models/openai/gpt-image-2) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/openai/gpt-image-2/edit/predictions" payload = json.loads("{\n \"prompt\": \"A blue ceramic cup on a wooden table\",\n \"resolution\": \"1k\",\n \"size\": \"1:1\",\n \"n\": 1,\n \"image_urls\": [\n \"https://example.com/reference.png\"\n ]\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/openai/gpt-image-2/edit/predictions"; const input = { "prompt": "A blue ceramic cup on a wooden table", "resolution": "1k", "size": "1:1", "n": 1, "image_urls": [ "https://example.com/reference.png" ] }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | prompt | Yes | string | — | min length 1 | Describe the desired content. | | resolution | resolution | No | string | — | "1k", "2k", "4k" | Output resolution. | | quality | quality | No | string | Form: "medium"; API: — | "auto", "low", "medium", "high" | Output quality. APIMart Official channel. auto is forwarded unchanged and uses the published high quality price tier. | | aspect_ratio | aspect_ratio | No | string | "1:1" | "1:1", "3:2", "2:3", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9", "9:21", "2:1", "1:2", "3:1", "1:3", "auto" | Output aspect ratio. | | images | images | Yes | string[] | — | min items 1; max items 16 | Official: PNG/JPEG/WebP, each image under 50 MB. No additional input pixel minimum is specified. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | n | n | No | integer | 1 | min 1; max 4 | Official: 1–4. Standard: 1. Each output is billed. | | mask_url | mask_url | No | string | — | uri | Mask: PNG under 4 MB with the same dimensions as the first image. The server checks matching dimensions before quoting. | | output_format | output_format | No | string | Form: "png"; API: — | "png", "jpeg", "webp" | Output file format. Official channel. | | size | size | No | string | — | — | Aspect ratio, auto, or WxH pixel dimensions; alias of aspect_ratio. Pixel dimensions use the published 4k price tier regardless of the resolution hint. | | image_urls | image_urls | No | string[] | — | max items 16 | Official: PNG/JPEG/WebP, each image under 50 MB. No additional input pixel minimum is specified. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | output_compression | output_compression | No | integer | — | min 0; max 100 | Official channel JPEG/WebP compression. | | nsfw_check | nsfw_check | No | boolean | — | true, false | Content-safety check setting. | | moderation | moderation | No | string | — | "auto", "low" | Official channel. | ```json { "type": "object", "properties": { "prompt": { "type": "string", "minLength": 1 }, "size": { "type": "string", "description": "Aspect ratio, auto, or WxH pixel dimensions; alias of aspect_ratio. Pixel dimensions use the published 4k price tier regardless of the resolution hint." }, "resolution": { "type": "string", "enum": [ "1k", "2k", "4k" ] }, "quality": { "type": "string", "enum": [ "auto", "low", "medium", "high" ], "description": "APIMart Official channel. auto is forwarded unchanged and uses the published high quality price tier." }, "n": { "type": "integer", "minimum": 1, "maximum": 4, "default": 1, "description": "Official: 1–4. Standard: 1. Each output is billed." }, "image_urls": { "type": "array", "items": { "type": "string", "x-image-rules": { "minSide": 1, "maxSide": 16384, "maxRatio": 0, "maxBytes": 20971520, "maxPixels": 40000000, "formats": [ "image/png", "image/jpeg", "image/webp" ], "rejectPngAlpha": false, "description": "Official: PNG/JPEG/WebP, each image under 50 MB. No additional input pixel minimum is specified. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created.", "source": "https://developers.openai.com/api/reference/python/resources/images/methods/edit" } }, "maxItems": 16, "description": "Official: PNG/JPEG/WebP, each image under 50 MB. No additional input pixel minimum is specified. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." }, "mask_url": { "type": "string", "format": "uri", "description": "Mask: PNG under 4 MB with the same dimensions as the first image. The server checks matching dimensions before quoting.", "x-image-rules": { "minSide": 1, "maxSide": 16384, "maxRatio": 0, "maxBytes": 3999999, "maxPixels": 40000000, "formats": [ "image/png" ], "rejectPngAlpha": false, "description": "Mask: PNG under 4 MB with the same dimensions as the first image. The server checks matching dimensions before quoting.", "source": "https://developers.openai.com/api/reference/python/resources/images/methods/edit" } }, "output_format": { "type": "string", "enum": [ "png", "jpeg", "webp" ], "description": "Official channel." }, "output_compression": { "type": "integer", "minimum": 0, "maximum": 100, "description": "Official channel JPEG/WebP compression." }, "nsfw_check": { "type": "boolean" }, "moderation": { "type": "string", "enum": [ "auto", "low" ], "description": "Official channel." } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged. Platform pricing: pixel sizes use the 4k SKU; quality=auto uses the high SKU while forwarding auto unchanged. URL references are forwarded directly. Inline references retain the Platform 6 MB aggregate request limit. The selected supplier is pinned; automatic Official fallback is disabled.", "x-supplier-notes": [ { "supplier": "apimart", "unsupported": [ "quality", "mask_url", "moderation", "output_compression", "n > 1", "image_urls > 15", "output_format other than png" ], "reason": "These controls are provided only by the APIMart Official image endpoint." }, { "supplier": "apimart_official", "unsupported": [ "n > 4", "image_urls > 16" ], "reason": "Published APIMart Official limits." }, { "supplier": "apimart / apimart_official", "unsupported": [ "stream=true", "response_format=b64_json", "partial_images" ], "reason": "These supplier endpoints return an asynchronous task ID and URL results; no streaming or base64 response contract is published." } ], "required": [ "prompt" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- # Seedance 2.0 Fast ## bytedance/seedance-2.0-fast/image-to-video Category: image-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-fast/image-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.0-fast/image-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.0-fast) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-fast/image-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"image_url\",\n \"role\": \"first_frame\",\n \"image_url\": {\n \"url\": \"https://example.com/reference.png\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"adaptive\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-fast/image-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | first_frame | content[].image_url.url | Yes | string | — | min length 1 | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=first_frame. | | last_frame | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=last_frame. | | aspect_ratio | ratio | No | string | "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | Output duration in seconds. 4–15 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–15 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## bytedance/seedance-2.0-fast/text-to-video Category: text-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-fast/text-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.0-fast/text-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.0-fast) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-fast/text-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-fast/text-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | Output duration in seconds. 4–15 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–15 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## bytedance/seedance-2.0-fast/omni-reference-to-video Category: Omni Reference to Video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-fast/omni-reference-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "video_url", "role": "reference_video", "video_url": { "url": "https://example.com/reference.mp4" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.0-fast/omni-reference-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.0-fast) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-fast/omni-reference-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"video_url\",\n \"role\": \"reference_video\",\n \"video_url\": {\n \"url\": \"https://example.com/reference.mp4\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0-fast/omni-reference-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "video_url", "role": "reference_video", "video_url": { "url": "https://example.com/reference.mp4" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | reference_videos | content[].video_url.url | No | string[] | [] | max items 3 | Reference video URLs. content item: type=video_url, role=reference_video. | | reference_images | content[].image_url.url | No | string[] | [] | max items 9 | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=reference_image. | | reference_audios | content[].audio_url.url | No | string[] | [] | max items 3 | Reference audio URLs. content item: type=audio_url, role=reference_audio. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | Output duration in seconds. 4–15 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Up to 9 reference images, 3 reference videos and 3 reference audios. Include at least one reference image or video; do not mix reference roles with first/last frames. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Up to 9 reference images, 3 reference videos and 3 reference audios. Include at least one reference image or video; do not mix reference roles with first/last frames.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–15 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- # Seedance 2.0 ## bytedance/seedance-2.0/image-to-video Category: image-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0/image-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.0/image-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.0) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0/image-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"image_url\",\n \"role\": \"first_frame\",\n \"image_url\": {\n \"url\": \"https://example.com/reference.png\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"adaptive\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0/image-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | first_frame | content[].image_url.url | Yes | string | — | min length 1 | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=first_frame. | | last_frame | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=last_frame. | | aspect_ratio | ratio | No | string | "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p", "1080p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | Output duration in seconds. 4–15 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–15 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p", "1080p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## bytedance/seedance-2.0/text-to-video Category: text-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0/text-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.0/text-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.0) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0/text-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0/text-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p", "1080p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | Output duration in seconds. 4–15 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–15 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p", "1080p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## bytedance/seedance-2.0/omni-reference-to-video Category: Omni Reference to Video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0/omni-reference-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "video_url", "role": "reference_video", "video_url": { "url": "https://example.com/reference.mp4" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/bytedance/seedance-2.0/omni-reference-to-video) · [Full documentation](/docs/models/bytedance/seedance-2.0) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0/omni-reference-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"video_url\",\n \"role\": \"reference_video\",\n \"video_url\": {\n \"url\": \"https://example.com/reference.mp4\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/bytedance/seedance-2.0/omni-reference-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "video_url", "role": "reference_video", "video_url": { "url": "https://example.com/reference.mp4" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | reference_videos | content[].video_url.url | Yes | string[] | [] | min items 1; max items 3 | Reference video URLs. content item: type=video_url, role=reference_video. | | reference_images | content[].image_url.url | No | string[] | [] | max items 9 | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=reference_image. | | reference_audios | content[].audio_url.url | No | string[] | [] | max items 3 | Reference audio URLs. content item: type=audio_url, role=reference_audio. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "480p", "720p", "1080p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 | Output duration in seconds. 4–15 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | Form: true; API: — | true, false | Whether to generate output audio. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1 | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url", "draft_task" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | content[].draft_task | content[].draft_task | No | object | — | — | Draft task reference. | | content[].draft_task.id | content[].draft_task.id | No | string | — | — | A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model. | | seed | seed | No | integer | — | — | Random seed. | | watermark | watermark | No | boolean | — | true, false | Whether to add a watermark. | | safety_identifier | safety_identifier | No | string | — | — | Caller-provided safety identifier. | | return_last_frame | return_last_frame | No | boolean | — | true, false | Whether to return the final frame. | | service_tier | service_tier | No | string | — | — | Requested processing tier. | | execution_expires_after | execution_expires_after | No | integer | — | — | Task execution expiry setting. | | priority | priority | No | integer | — | — | Task processing priority. | | draft | draft | No | boolean | — | true, false | Whether to request draft generation. | | camera_fixed | camera_fixed | No | boolean | — | true, false | Whether to use a fixed camera. | | output_format | output_format | No | string | — | — | Output file format. | | omni_reference_task_type | omni_reference_task_type | No | string | — | — | Reference-generation task type. | | frames | frames | No | integer | — | — | Requested number of video frames. | | tools | tools | No | object[] | — | — | Model tool configuration. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url", "draft_task" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 300–6000 px per side; width/height 0.4–2.5; each image under 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "draft_task": { "type": "object", "properties": { "id": { "type": "string", "description": "A succeeded draft prediction ID owned by this Platform team and created by the same supplier/model." } } } } }, "minItems": 1 }, "duration": { "type": "integer", "default": 5, "description": "4–15 seconds or -1 for automatic duration; default 5." }, "resolution": { "type": "string", "enum": [ "480p", "720p", "1080p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "seed": { "type": "integer" }, "watermark": { "type": "boolean" }, "generate_audio": { "type": "boolean" }, "safety_identifier": { "type": "string" }, "return_last_frame": { "type": "boolean" }, "service_tier": { "type": "string" }, "execution_expires_after": { "type": "integer" }, "priority": { "type": "integer" }, "draft": { "type": "boolean" }, "camera_fixed": { "type": "boolean" }, "output_format": { "type": "string" }, "omni_reference_task_type": { "type": "string" }, "frames": { "type": "integer" }, "tools": { "type": "array", "items": { "type": "object" } } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "required": [ "content" ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- # Minimax H3 ## minimax/minimax-h3/image-to-video Category: image-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/minimax/minimax-h3/image-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "768P", "ratio": "adaptive" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/minimax/minimax-h3/image-to-video) · [Full documentation](/docs/models/minimax/minimax-h3) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/minimax/minimax-h3/image-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"image_url\",\n \"role\": \"first_frame\",\n \"image_url\": {\n \"url\": \"https://example.com/reference.png\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"768P\",\n \"ratio\": \"adaptive\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/minimax/minimax-h3/image-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "768P", "ratio": "adaptive" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | first_frame | content[].image_url.url | Yes | string | — | min length 1 | Official: 256–5760 px per side; width/height 0.4–2.5; at most 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=first_frame. | | last_frame | content[].image_url.url | No | string | — | — | Official: 256–5760 px per side; width/height 0.4–2.5; at most 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=last_frame. | | aspect_ratio | ratio | No | string | Form: "adaptive"; API: — | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. Required and non-adaptive for text-only requests; optional for reference input; frame input uses adaptive upstream. | | resolution | resolution | No | string | "768P" | "768P", "2K" | Output resolution. | | duration | duration | No | integer | Form: 5; API: — | min 4; max 15 | Output duration in seconds. 4–15 seconds; default 5 in Platform. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | No | object[] | — | — | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 256–5760 px per side; width/height 0.4–2.5; at most 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | aigc_watermark | aigc_watermark | No | boolean | false | true, false | Whether to add an AI-generated-content watermark. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 256–5760 px per side; width/height 0.4–2.5; at most 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } } } } }, "duration": { "type": "integer", "description": "4–15 seconds; default 5 in Platform." }, "resolution": { "type": "string", "enum": [ "768P", "2K" ], "default": "768P" }, "ratio": { "type": "string", "description": "Required and non-adaptive for text-only requests; optional for reference input; frame input uses adaptive upstream." }, "aigc_watermark": { "type": "boolean", "default": false } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "x-supplier-notes": [ { "supplier": "minimax", "unsupported": [ "seed", "prompt_optimizer", "extra.prompt_expansion_mode" ], "reason": "The H3 V2 contract does not expose seed or the legacy prompt_optimizer; prompt_expansion_mode is exclusive to H3 Max, which is outside this model." } ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## minimax/minimax-h3/text-to-video Category: text-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/minimax/minimax-h3/text-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "768P", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/minimax/minimax-h3/text-to-video) · [Full documentation](/docs/models/minimax/minimax-h3) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/minimax/minimax-h3/text-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n }\n ],\n \"duration\": 5,\n \"resolution\": \"768P\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/minimax/minimax-h3/text-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "768P", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: — | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. Required and non-adaptive for text-only requests; optional for reference input; frame input uses adaptive upstream. | | resolution | resolution | No | string | "768P" | "768P", "2K" | Output resolution. | | duration | duration | No | integer | Form: 5; API: — | min 4; max 15 | Output duration in seconds. 4–15 seconds; default 5 in Platform. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | No | object[] | — | — | Official multimodal content, forwarded without renaming. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | Official: 256–5760 px per side; width/height 0.4–2.5; at most 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | | aigc_watermark | aigc_watermark | No | boolean | false | true, false | Whether to add an AI-generated-content watermark. | ```json { "type": "object", "properties": { "content": { "type": "array", "description": "Official multimodal content, forwarded without renaming.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "Official: 256–5760 px per side; width/height 0.4–2.5; at most 30 MB. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } } } } }, "duration": { "type": "integer", "description": "4–15 seconds; default 5 in Platform." }, "resolution": { "type": "string", "enum": [ "768P", "2K" ], "default": "768P" }, "ratio": { "type": "string", "description": "Required and non-adaptive for text-only requests; optional for reference input; frame input uses adaptive upstream." }, "aigc_watermark": { "type": "boolean", "default": false } }, "additionalProperties": false, "description": "Native request fields. The model route supplies model when omitted. Platform authentication, idempotency and asynchronous prediction responses remain unchanged.", "x-supplier-notes": [ { "supplier": "minimax", "unsupported": [ "seed", "prompt_optimizer", "extra.prompt_expansion_mode" ], "reason": "The H3 V2 contract does not expose seed or the legacy prompt_optimizer; prompt_expansion_mode is exclusive to H3 Max, which is outside this model." } ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- # Topview T1 ## topview/topview-t1/image-to-video Category: image-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/topview/topview-t1/image-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/topview/topview-t1/image-to-video) · [Full documentation](/docs/models/topview/topview-t1) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/topview/topview-t1/image-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"image_url\",\n \"role\": \"first_frame\",\n \"image_url\": {\n \"url\": \"https://example.com/reference.png\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"adaptive\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/topview/topview-t1/image-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "first_frame", "image_url": { "url": "https://example.com/reference.png" } } ], "duration": 5, "resolution": "720p", "ratio": "adaptive" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | first_frame | content[].image_url.url | Yes | string | — | min length 1 | The current T1 version has no verified public image-dimension specification; Platform limits below apply. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=first_frame. | | last_frame | content[].image_url.url | No | string | — | — | The current T1 version has no verified public image-dimension specification; Platform limits below apply. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=last_frame. | | aspect_ratio | ratio | No | string | "adaptive" | "adaptive" | Output aspect ratio. Output aspect ratio follows the first frame. | | resolution | resolution | No | string | "720p" | "720p", "1080p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 | Output duration in seconds. 4–30 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | true | true, false | On: include audio. Off: no audio track. Default true: generate an output audio track. Set false for video without an audio track, even when reference audio is supplied. Reference audio is an input cue, not a promise to copy or preserve its original soundtrack. Both settings use the same current per-second rate. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1; max items 51 | Text prompt plus up to 30 images, 10 videos and 10 audios. First/last frames cannot be combined with reference roles. A last frame requires a first frame. For omni-reference-to-video, reference audio uses type=audio_url, role=reference_audio and audio_url.url; it is supported with generate_audio either true or false. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | The current T1 version has no verified public image-dimension specification; Platform limits below apply. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | ```json { "type": "object", "additionalProperties": false, "required": [ "content" ], "description": "Topview T1. Public HTTP(S) media URLs only; inline base64, asset:// and draft tasks are not supported. Billing uses supplier-reported input video plus output video seconds; reference images and audio do not add billable seconds. generate_audio does not change the current per-second rate. Submitting an accepted quote reserves funds; final charged_usd follows actual usage, and unused funds are released. Check the current quote for pricing. Download temporary result URLs promptly.", "properties": { "content": { "type": "array", "description": "Text prompt plus up to 30 images, 10 videos and 10 audios. First/last frames cannot be combined with reference roles. A last frame requires a first frame. For omni-reference-to-video, reference audio uses type=audio_url, role=reference_audio and audio_url.url; it is supported with generate_audio either true or false.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "The current T1 version has no verified public image-dimension specification; Platform limits below apply. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } } } }, "minItems": 1, "maxItems": 51 }, "duration": { "type": "integer", "description": "4–30 seconds or -1 for automatic duration; default 5.", "default": 5 }, "resolution": { "type": "string", "enum": [ "720p", "1080p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive" ], "default": "adaptive", "description": "Output aspect ratio follows the first frame." }, "generate_audio": { "type": "boolean", "default": true, "description": "Default true: generate an output audio track. Set false for video without an audio track, even when reference audio is supplied. Reference audio is an input cue, not a promise to copy or preserve its original soundtrack. Both settings use the same current per-second rate." } }, "x-supplier-notes": [ { "supplier": "Topview T1", "unsupported": [ "seed", "watermark", "return_last_frame", "draft", "camera_fixed", "priority", "negative_prompt", "enhance_prompt", "input_region", "session_context", "frames", "tools" ], "reason": "The current T1 gateway does not map these fields. Unsupported fields are rejected instead of silently ignored." } ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## topview/topview-t1/omni-reference-to-video Category: Omni Reference to Video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/topview/topview-t1/omni-reference-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "reference_image", "image_url": { "url": "https://example.com/reference.png" } }, { "type": "audio_url", "role": "reference_audio", "audio_url": { "url": "https://example.com/reference.wav" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9", "generate_audio": true }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/topview/topview-t1/omni-reference-to-video) · [Full documentation](/docs/models/topview/topview-t1) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/topview/topview-t1/omni-reference-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n },\n {\n \"type\": \"image_url\",\n \"role\": \"reference_image\",\n \"image_url\": {\n \"url\": \"https://example.com/reference.png\"\n }\n },\n {\n \"type\": \"audio_url\",\n \"role\": \"reference_audio\",\n \"audio_url\": {\n \"url\": \"https://example.com/reference.wav\"\n }\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\",\n \"generate_audio\": true\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/topview/topview-t1/omni-reference-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" }, { "type": "image_url", "role": "reference_image", "image_url": { "url": "https://example.com/reference.png" } }, { "type": "audio_url", "role": "reference_audio", "audio_url": { "url": "https://example.com/reference.wav" } } ], "duration": 5, "resolution": "720p", "ratio": "16:9", "generate_audio": true }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | reference_videos | content[].video_url.url | No | string[] | [] | max items 10 | Reference video URLs. content item: type=video_url, role=reference_video. | | reference_images | content[].image_url.url | No | string[] | [] | max items 30 | The current T1 version has no verified public image-dimension specification; Platform limits below apply. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. content item: type=image_url, role=reference_image. | | reference_audios | content[].audio_url.url | No | string[] | [] | max items 10 | Up to 10 public HTTP(S) audio URLs. Supported with Generate audio on or off; reference audio does not add billable seconds. content item: type=audio_url, role=reference_audio. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "720p", "1080p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 | Output duration in seconds. 4–30 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | true | true, false | On: include audio. Off: no audio track. Default true: generate an output audio track. Set false for video without an audio track, even when reference audio is supplied. Reference audio is an input cue, not a promise to copy or preserve its original soundtrack. Both settings use the same current per-second rate. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1; max items 51 | Text prompt plus up to 30 images, 10 videos and 10 audios. First/last frames cannot be combined with reference roles. A last frame requires a first frame. For omni-reference-to-video, reference audio uses type=audio_url, role=reference_audio and audio_url.url; it is supported with generate_audio either true or false. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | The current T1 version has no verified public image-dimension specification; Platform limits below apply. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | ```json { "type": "object", "additionalProperties": false, "required": [ "content" ], "description": "Topview T1. Public HTTP(S) media URLs only; inline base64, asset:// and draft tasks are not supported. Billing uses supplier-reported input video plus output video seconds; reference images and audio do not add billable seconds. generate_audio does not change the current per-second rate. Submitting an accepted quote reserves funds; final charged_usd follows actual usage, and unused funds are released. Check the current quote for pricing. Download temporary result URLs promptly.", "properties": { "content": { "type": "array", "description": "Text prompt plus up to 30 images, 10 videos and 10 audios. First/last frames cannot be combined with reference roles. A last frame requires a first frame. For omni-reference-to-video, reference audio uses type=audio_url, role=reference_audio and audio_url.url; it is supported with generate_audio either true or false.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "The current T1 version has no verified public image-dimension specification; Platform limits below apply. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } } } }, "minItems": 1, "maxItems": 51 }, "duration": { "type": "integer", "description": "4–30 seconds or -1 for automatic duration; default 5.", "default": 5 }, "resolution": { "type": "string", "enum": [ "720p", "1080p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "generate_audio": { "type": "boolean", "default": true, "description": "Default true: generate an output audio track. Set false for video without an audio track, even when reference audio is supplied. Reference audio is an input cue, not a promise to copy or preserve its original soundtrack. Both settings use the same current per-second rate." } }, "x-supplier-notes": [ { "supplier": "Topview T1", "unsupported": [ "seed", "watermark", "return_last_frame", "draft", "camera_fixed", "priority", "negative_prompt", "enhance_prompt", "input_region", "session_context", "frames", "tools" ], "reason": "The current T1 gateway does not map these fields. Unsupported fields are rejected instead of silently ignored." } ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission. --- ## topview/topview-t1/text-to-video Category: text-to-video Status: available. Follow the executable parameters and server quote. ## Submit and poll ```bash # Set TOPVIEW_API_PLATFORM_KEY once. Preserve REQUEST_KEY and input for uncertain retries. # For a NEW task only, unset REQUEST_KEY first. Save data.id to resume GET only. : "${TOPVIEW_API_PLATFORM_KEY:?Set TOPVIEW_API_PLATFORM_KEY first}" export REQUEST_KEY="${REQUEST_KEY:-$(uuidgen)}" curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/models/topview/topview-t1/text-to-video/predictions' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" \ -H "Idempotency-Key: $REQUEST_KEY" \ -H 'Content-Type: application/json' \ --data '{ "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }' # Poll data.id until completed or failed. curl --fail-with-body 'https://platform-backend.topview.ai/api/v1/predictions/REQUEST_ID' \ -H "Authorization: Bearer $TOPVIEW_API_PLATFORM_KEY" ``` [Playground](/models/topview/topview-t1/text-to-video) · [Full documentation](/docs/models/topview/topview-t1) · [Developer guides](/docs) ## Python ```python # Python 3.10+ · pip install requests # Set TOPVIEW_API_PLATFORM_KEY. Use the original creating key to query an API task. # TASK_ID resumes GET only. REQUEST_STATE stores input + idempotency key, never credentials. import json, os, time, uuid, tempfile from pathlib import Path from email.utils import parsedate_to_datetime import requests api_key = os.environ.get("TOPVIEW_API_PLATFORM_KEY") or os.environ.get("TOPVIEW_API_KEY") or os.environ.get("PLATFORM_API_KEY") if not api_key: raise RuntimeError("Set TOPVIEW_API_PLATFORM_KEY") headers = {"Authorization": "Bearer " + api_key, "Content-Type": "application/json"} endpoint = "https://platform-backend.topview.ai/api/v1/models/topview/topview-t1/text-to-video/predictions" payload = json.loads("{\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"A blue paper boat floating on a calm pond\"\n }\n ],\n \"duration\": 5,\n \"resolution\": \"720p\",\n \"ratio\": \"16:9\"\n}") state_file = Path(os.environ.get("REQUEST_STATE", ".topview-request.json")) task_id = os.environ.get("TASK_ID") if not task_id: if state_file.exists(): state = json.loads(state_file.read_text()) if state["endpoint"] != endpoint or state["input"] != payload: raise RuntimeError("Saved request differs. Resume its TASK_ID, or use a new REQUEST_STATE for a new task.") if os.environ.get("REQUEST_KEY") and os.environ["REQUEST_KEY"] != state["request_key"]: raise RuntimeError("REQUEST_KEY differs from saved state; keep the original key.") else: state = {"endpoint": endpoint, "input": payload, "request_key": os.environ.get("REQUEST_KEY") or str(uuid.uuid4())} # Exclusive creation fails safely if another process starts this request concurrently. fd = os.open(state_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w") as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()) task_id = state.get("task_id") if not task_id: print("REQUEST_KEY=" + state["request_key"], flush=True) # One POST only. On timeout, rerun with this state file: same input, same key. response = requests.post(endpoint, headers={**headers, "Idempotency-Key": state["request_key"]}, json=state["input"], timeout=120) response.raise_for_status() body = response.json() if body.get("code") != 200: raise RuntimeError(body) task_id = body["data"]["id"] state["task_id"] = task_id with tempfile.NamedTemporaryFile(mode="w", dir=state_file.parent, delete=False) as f: json.dump(state, f); f.flush(); os.fsync(f.fileno()); temporary = f.name os.replace(temporary, state_file) print("TASK_ID=" + task_id, flush=True) if not task_id.isalnum(): raise ValueError("Invalid TASK_ID") deadline = time.monotonic() + 1800 failures, interval = 0, 3 while time.monotonic() < deadline: response = None try: response = requests.get("https://platform-backend.topview.ai/api/v1/predictions/" + task_id, headers=headers, timeout=min(60, max(1, deadline - time.monotonic()))) except requests.exceptions.RequestException: pass # Polling can be retried safely; submission is never repeated here. if response is None or response.status_code == 429 or response.status_code >= 500: failures += 1 if failures > 8: raise RuntimeError("Polling unavailable. Resume with TASK_ID=" + task_id) delay = min(60, 2 ** failures) if response is not None: retry_after = response.headers.get("Retry-After", "") try: seconds = float(retry_after) if retry_after.isdigit() else parsedate_to_datetime(retry_after).timestamp() - time.time() delay = max(delay, seconds) except (ValueError, TypeError, OverflowError): pass time.sleep(min(delay, max(0, deadline - time.monotonic()))) continue response.raise_for_status() # 401/403/404 are not transient failures. body = response.json() if body.get("code") != 200: raise RuntimeError(body) failures = 0 task = body["data"] if task["status"] == "completed": print(json.dumps(task, indent=2)) # outputs, charged_usd, billing_state break if task["status"] == "payment_required": raise RuntimeError("Top up the workspace and resume TASK_ID=" + task_id + ": " + json.dumps(task)) if task["status"] == "failed": raise RuntimeError(task) # Inspect billing_state for release status. time.sleep(min(interval, max(0, deadline - time.monotonic()))) interval = min(15, interval * 1.25) else: raise TimeoutError("Resume TASK_ID=" + task_id + "; do not submit a new generation.") # Keep REQUEST_STATE until you have saved the result. A new state path starts a new task. ``` ## JavaScript ```javascript // Node.js 20+ · save as example.mjs; set TOPVIEW_API_PLATFORM_KEY. // TASK_ID resumes GET only. Use the original creating key, including after app restarts. import { randomUUID } from 'node:crypto'; import { existsSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync } from 'node:fs'; const apiKey = process.env.TOPVIEW_API_PLATFORM_KEY || process.env.TOPVIEW_API_KEY || process.env.PLATFORM_API_KEY; if (!apiKey) throw new Error('Set TOPVIEW_API_PLATFORM_KEY'); const headers = { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }; const endpoint = "https://platform-backend.topview.ai/api/v1/models/topview/topview-t1/text-to-video/predictions"; const input = { "content": [ { "type": "text", "text": "A blue paper boat floating on a calm pond" } ], "duration": 5, "resolution": "720p", "ratio": "16:9" }; const statePath = process.env.REQUEST_STATE || '.topview-request.json'; let taskId = process.env.TASK_ID; if (!taskId) { let state; if (existsSync(statePath)) { state = JSON.parse(readFileSync(statePath, 'utf8')); if (state.endpoint !== endpoint || JSON.stringify(state.input) !== JSON.stringify(input)) throw new Error('Saved request differs. Resume its TASK_ID or choose a new REQUEST_STATE for a new task.'); if (process.env.REQUEST_KEY && process.env.REQUEST_KEY !== state.request_key) throw new Error('REQUEST_KEY differs from saved state; keep the original key.'); } else { state = { endpoint, input, request_key: process.env.REQUEST_KEY || randomUUID() }; const fd = openSync(statePath, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } } taskId = state.task_id; if (!taskId) { console.log('REQUEST_KEY=' + state.request_key); // One POST only. An uncertain result must reuse this file, input and key. const response = await fetch(endpoint, { method: 'POST', headers: { ...headers, 'Idempotency-Key': state.request_key }, body: JSON.stringify(state.input), signal: AbortSignal.timeout(120000) }); if (!response.ok) throw new Error('Submission HTTP ' + response.status + '; keep REQUEST_STATE and REQUEST_KEY.'); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); taskId = body.data.id; state.task_id = taskId; const temporary = statePath + '.' + randomUUID() + '.tmp'; const fd = openSync(temporary, 'wx', 0o600); try { writeFileSync(fd, JSON.stringify(state)); fsyncSync(fd); } finally { closeSync(fd); } renameSync(temporary, statePath); } } console.log('TASK_ID=' + taskId); if (!/^[a-zA-Z0-9]+$/.test(taskId)) throw new Error('Invalid TASK_ID'); const deadline = Date.now() + 1800000; const sleep = ms => new Promise(resolve => setTimeout(resolve, Math.min(ms, Math.max(0, deadline - Date.now())))); let failures = 0, interval = 3000, completed = false; while (Date.now() < deadline) { let response; try { response = await fetch("https://platform-backend.topview.ai/api/v1/predictions/" + encodeURIComponent(taskId), { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(60000, deadline - Date.now()))) }); } catch { /* Network errors during GET are safe to retry. */ } if (!response || response.status === 429 || response.status >= 500) { if (++failures > 8) throw new Error('Polling unavailable. Resume TASK_ID=' + taskId); let delay = Math.min(60000, 1000 * 2 ** failures); const retryAfter = response?.headers.get('Retry-After'); if (retryAfter) { const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1000; if (Number.isFinite(seconds)) delay = Math.max(delay, seconds * 1000); } await sleep(delay); continue; } if (!response.ok) throw new Error('Polling HTTP ' + response.status + '; check original key and TASK_ID=' + taskId); const body = await response.json(); if (body.code !== 200) throw new Error(JSON.stringify(body)); failures = 0; const task = body.data; if (task.status === 'completed') { console.log(task); completed = true; break; } if (task.status === 'payment_required') throw new Error('Top up and resume TASK_ID=' + taskId + ': ' + JSON.stringify(task)); if (task.status === 'failed') throw new Error(JSON.stringify(task)); await sleep(interval); interval = Math.min(15000, interval * 1.25); } if (!completed) throw new Error('Resume TASK_ID=' + taskId + '; do not submit another generation.'); // State files contain your input. Keep them private; use a new REQUEST_STATE for a new task. ``` ## Result Guide Save data.id after submission. New or rotated keys cannot query tasks created by another key. View Playground tasks and tasks whose original key is no longer active in authorized workspace web history. Python and JavaScript examples save input and idempotency key in REQUEST_STATE before submitting; restart with the same file or set TASK_ID for GET-only recovery. Keep that file private; choose a new path for a new task. Poll GET /api/v1/predictions/{id} with the original creating API key (requests:read and the corresponding mode permission) every 2–5 seconds; back off for long-running tasks and 429 / 5xx responses. Continue on created / processing. On payment_required, inspect amount_due_usd, additional_charge_usd and shortfall_usd. Top up the same workspace; settlement retries automatically without resubmitting. The estimated price is not a cap: actual usage above the reservation is automatically charged when sufficient eligible balance is available. On completed, read data.outputs. On failed, read data.error and stop polling. Read data.charged_usd for the actual task cost in USD (an exact decimal string); data.billing_state = CAPTURED confirms settlement. No estimate request is required. Save media promptly because URLs may expire. A timeout is not a confirmed failure: query the original task; do not submit a duplicate. ## Input schema Common fields match Playground. Use the API path for request JSON. — means no declared default. | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | prompt | content[].text | Yes | string | — | min length 1; max length 10000 | Describe the desired content. content item: type=text. | | aspect_ratio | ratio | No | string | Form: "16:9"; API: "adaptive" | "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" | Output aspect ratio. | | resolution | resolution | No | string | "720p" | "720p", "1080p" | Output resolution. | | duration | duration | No | integer | 5 | -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 | Output duration in seconds. 4–30 seconds or -1 for automatic duration; default 5. | | generate_audio | generate_audio | No | boolean | true | true, false | On: include audio. Off: no audio track. Default true: generate an output audio track. Set false for video without an audio track, even when reference audio is supplied. Reference audio is an input cue, not a promise to copy or preserve its original soundtrack. Both settings use the same current per-second rate. |
Advanced parameters and complete request structure | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | content | content | Yes | object[] | — | min items 1; max items 51 | Text prompt plus up to 30 images, 10 videos and 10 audios. First/last frames cannot be combined with reference roles. A last frame requires a first frame. For omni-reference-to-video, reference audio uses type=audio_url, role=reference_audio and audio_url.url; it is supported with generate_audio either true or false. | | content[].type | content[].type | No | string | — | "text", "image_url", "video_url", "audio_url" | Content or media item type. | | content[].text | content[].text | No | string | — | — | Text to synthesize or process. | | content[].role | content[].role | No | string | — | — | Media role within the generation request. | | content[].image_url | content[].image_url | No | object | — | — | Image URL object. | | content[].image_url.url | content[].image_url.url | No | string | — | — | The current T1 version has no verified public image-dimension specification; Platform limits below apply. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created. | | content[].video_url | content[].video_url | No | object | — | — | Video URL object. | | content[].video_url.url | content[].video_url.url | No | string | — | — | Public media URL. | | content[].audio_url | content[].audio_url | No | object | — | — | Audio URL object. | | content[].audio_url.url | content[].audio_url.url | No | string | — | — | Public media URL. | ```json { "type": "object", "additionalProperties": false, "required": [ "content" ], "description": "Topview T1. Public HTTP(S) media URLs only; inline base64, asset:// and draft tasks are not supported. Billing uses supplier-reported input video plus output video seconds; reference images and audio do not add billable seconds. generate_audio does not change the current per-second rate. Submitting an accepted quote reserves funds; final charged_usd follows actual usage, and unused funds are released. Check the current quote for pricing. Download temporary result URLs promptly.", "properties": { "content": { "type": "array", "description": "Text prompt plus up to 30 images, 10 videos and 10 audios. First/last frames cannot be combined with reference roles. A last frame requires a first frame. For omni-reference-to-video, reference audio uses type=audio_url, role=reference_audio and audio_url.url; it is supported with generate_audio either true or false.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "text", "image_url", "video_url", "audio_url" ] }, "text": { "type": "string" }, "role": { "type": "string" }, "image_url": { "type": "object", "properties": { "url": { "type": "string", "description": "The current T1 version has no verified public image-dimension specification; Platform limits below apply. Platform input: PNG/JPEG/WebP only, at most 20 MiB (20,971,520 bytes), at most 40 megapixels. Checks run before a quote is created." } } }, "video_url": { "type": "object", "properties": { "url": { "type": "string" } } }, "audio_url": { "type": "object", "properties": { "url": { "type": "string" } } } } }, "minItems": 1, "maxItems": 51 }, "duration": { "type": "integer", "description": "4–30 seconds or -1 for automatic duration; default 5.", "default": 5 }, "resolution": { "type": "string", "enum": [ "720p", "1080p" ], "default": "720p" }, "ratio": { "type": "string", "enum": [ "adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9" ], "default": "adaptive" }, "generate_audio": { "type": "boolean", "default": true, "description": "Default true: generate an output audio track. Set false for video without an audio track, even when reference audio is supplied. Reference audio is an input cue, not a promise to copy or preserve its original soundtrack. Both settings use the same current per-second rate." } }, "x-supplier-notes": [ { "supplier": "Topview T1", "unsupported": [ "seed", "watermark", "return_last_frame", "draft", "camera_fixed", "priority", "negative_prompt", "enhance_prompt", "input_region", "session_context", "frames", "tools" ], "reason": "The current T1 gateway does not map these fields. Unsupported fields are rejected instead of silently ignored." } ] } ```
## Output schema | Parameter | API path | Required | Type | Default | Options / limits | Description | |---|---|---|---|---|---|---| | code | code | Yes | integer | — | — | HTTP API envelope code. Inspect data.status for the task outcome. | | message | message | Yes | string | — | — | API response message. | | data | data | Yes | object | — | — | Prediction task details. | | data.id | data.id | Yes | string | — | — | Platform task ID. Persist it before polling. | | data.created_at | data.created_at | Yes | string | — | date-time | Task creation timestamp. | | data.code | data.code | Yes | integer | — | — | 0 for normal tasks; 402 when payment is required; 1403 for a failed task. | | data.execution_state | data.execution_state | Yes | string | — | — | Execution state; inspect status and billing_state before treating a task as settled. | | data.status | data.status | Yes | string | — | "created", "processing", "payment_required", "completed", "failed" | Current task status. | | data.outputs | data.outputs | Yes | string[] | — | — | Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix. | | data.amount_due_usd | data.amount_due_usd | No | string | — | — | Full actual cost in USD, present when status is payment_required. | | data.additional_charge_usd | data.additional_charge_usd | No | string | — | — | Actual cost minus the original reservation; present when payment is required. | | data.shortfall_usd | data.shortfall_usd | No | string | — | — | Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed. | | data.charged_usd | data.charged_usd | Yes | string | — | — | Actual USD charge as an exact decimal string, not the reserved amount. | | data.billing_state | data.billing_state | Yes | string | — | "RESERVED", "CAPTURED", "RELEASED" | RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income. | | data.error | data.error | Yes | string | — | — | Failure or payment-required description, otherwise an empty string. | | data.urls | data.urls | Yes | object | — | — | Task-related API URLs. | | data.urls.get | data.urls.get | Yes | string | — | uri | Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission. | | data.last_frame_url | data.last_frame_url | No | string | — | uri | Last-frame image URL when requested and returned, available only after settlement. |
Raw output JSON Schema ```json { "type": "object", "required": [ "code", "message", "data" ], "properties": { "code": { "type": "integer", "description": "HTTP API envelope code. Inspect data.status for the task outcome." }, "message": { "type": "string" }, "data": { "type": "object", "required": [ "id", "created_at", "code", "execution_state", "status", "outputs", "charged_usd", "billing_state", "error", "urls" ], "properties": { "id": { "type": "string", "description": "Platform task ID. Persist it before polling." }, "created_at": { "type": "string", "format": "date-time" }, "code": { "type": "integer", "description": "0 for normal tasks; 402 when payment is required; 1403 for a failed task." }, "execution_state": { "type": "string", "description": "Execution state; inspect status and billing_state before treating a task as settled." }, "status": { "type": "string", "enum": [ "created", "processing", "payment_required", "completed", "failed" ] }, "outputs": { "type": "array", "items": { "type": "string" }, "description": "Empty until settled successfully. HTTPS result URLs or image data; enable_base64_output returns raw Base64 without a data URI prefix." }, "amount_due_usd": { "type": "string", "description": "Full actual cost in USD, present when status is payment_required." }, "additional_charge_usd": { "type": "string", "description": "Actual cost minus the original reservation; present when payment is required." }, "shortfall_usd": { "type": "string", "description": "Additional eligible balance needed at the latest settlement attempt. Top up this workspace; settlement retries automatically. No duplicate submission is needed." }, "charged_usd": { "type": "string", "description": "Actual USD charge as an exact decimal string, not the reserved amount.", "pattern": "^[0-9]+(\\.[0-9]+)?$", "example": "0.040000000000" }, "billing_state": { "type": "string", "description": "RESERVED is a temporary hold, not spending. CAPTURED is the final charge. RELEASED returns the full hold after failure; released funds are not recharge income.", "enum": [ "RESERVED", "CAPTURED", "RELEASED" ] }, "error": { "type": "string", "description": "Failure or payment-required description, otherwise an empty string." }, "urls": { "type": "object", "required": [ "get" ], "properties": { "get": { "type": "string", "format": "uri", "description": "Authenticated polling URL. Use the original creating API key with requests:read and the corresponding mode permission." } } }, "last_frame_url": { "type": "string", "format": "uri", "description": "Last-frame image URL when requested and returned, available only after settlement." } } } } } ```
## Usage - [Quick Start](/docs/quickstart) - [API Authentication](/docs/authentication) - [Upload an asset](/docs/assets-upload) - [Import an asset URL](/docs/assets-import) - [List assets](/docs/assets-list) - [Get an asset](/docs/assets-get) Follow the constraints on this page. Unsupported parameters are validated before submission.