# 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.