Skip to main content

Available models

Use Seedance in the dashboard

Open model page
ModelModel IDMax durationAspect ratiosResolutions
Seedance 2.0doubao/doubao-seedance-2-0-26012860s16:9 / 9:16 / 1:1 / 4:3 / 3:4480p / 720p / 1080p

Billing

Video generation is billed by pixel-tokens, following the official Volcengine formula:
tokens = duration (s) × width (px) × height (px) × 24 fps ÷ 1024
cost   = tokens ÷ 1,000,000 × price_per_million
Token reference table (pure generation, 16:9):
ResolutionTokens / sec5s video10s video
480p (848×480)~9,540~47,700~95,400
720p (1280×720)~21,600~108,000~216,000
1080p (1920×1080)~48,600~243,000~486,000
Check your dashboard for the current per-million-token price for each model.
Note: Only successful generations are charged. Tasks that fail due to content policy or upstream errors incur no cost. Billing is reconciled against the actual token count reported by the upstream API on task completion.

How it works

Video generation is a two-step async process: submit a task, then poll for the result.
1

Submit a generation task

Send a POST request to /v1/generation/tasks:
curl https://oss.chatgpttech.mobi/v1/generation/tasks \
  -H "Authorization: Bearer sk-hub-your-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "doubao/doubao-seedance-2-0-260128",
    "content": [{"type": "text", "text": "A beautiful sunset over mountains"}],
    "ratio": "16:9",
    "resolution": "720p",
    "duration": 5
  }'
const task = await fetch('https://oss.chatgpttech.mobi/v1/generation/tasks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk-hub-your-key-here',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'doubao/doubao-seedance-2-0-260128',
    content: [{ type: 'text', text: 'A beautiful sunset over mountains' }],
    ratio: '16:9',
    resolution: '720p',
    duration: 5,
  }),
}).then(r => r.json());

console.log(task.id); // use this to poll
import requests, time

res = requests.post(
    'https://oss.chatgpttech.mobi/v1/generation/tasks',
    headers={'Authorization': 'Bearer sk-hub-your-key-here'},
    json={
        'model': 'doubao/doubao-seedance-2-0-260128',
        'content': [{'type': 'text', 'text': 'A beautiful sunset over mountains'}],
        'ratio': '16:9',
        'resolution': '720p',
        'duration': 5,
    }
)
task = res.json()
print(task['id'])
The response returns a task ID you’ll use to check progress:
{
  "id": "task_abc123",
  "status": "queued"
}
2

Poll for the result

Query the task status using the returned id. Poll every 3–5 seconds until the status is succeeded or failed:
curl https://oss.chatgpttech.mobi/v1/generation/tasks/task_abc123 \
  -H "Authorization: Bearer sk-hub-your-key-here"
let result: any = { status: 'queued' };
while (result.status === 'queued' || result.status === 'running') {
  await new Promise(r => setTimeout(r, 4000));
  result = await fetch(
    `https://oss.chatgpttech.mobi/v1/generation/tasks/${task.id}`,
    { headers: { 'Authorization': 'Bearer sk-hub-your-key-here' } }
  ).then(r => r.json());
}

if (result.status === 'succeeded') {
  console.log('Video URL:', result.content.video_url);
}
import time

while True:
    time.sleep(4)
    result = requests.get(
        f"https://oss.chatgpttech.mobi/v1/generation/tasks/{task['id']}",
        headers={'Authorization': 'Bearer sk-hub-your-key-here'}
    ).json()
    if result['status'] not in ('queued', 'running'):
        break

if result['status'] == 'succeeded':
    print('Video URL:', result['content']['video_url'])
Completed response:
{
  "id": "task_abc123",
  "status": "succeeded",
  "content": {
    "video_url": "https://cdn.example.com/output/abc123.mp4"
  },
  "usage": {
    "completion_tokens": 108000
  }
}
content on the response is a single object ({ video_url }), not the part-array format used in the request content field. Don’t reuse your request-parsing code for the response.

Task status reference

StatusDescription
queuedWaiting to start
runningGeneration in progress
succeededDone — read the URL from content.video_url
failedGeneration failed — see the error field for details

Image-to-video

To animate a still image instead of generating from text alone, add an image_url part to the content array alongside your text prompt. There are three ways to supply that image, in order of preference:

1. Upload the file

Recommended. Small, reliable request bodies.

2. Public image URL

Simplest, if the image is already hosted.

3. Base64 data URI

Quick one-off tests only.
POST /v1/assets/upload with a multipart/form-data body containing the file and the target model. It returns a hosted assetUrl you pass straight into image_url — your generation request body stays small no matter how large the source photo is.
# Step 1: upload the image
curl https://oss.chatgpttech.mobi/v1/assets/upload \
  -H "Authorization: Bearer sk-hub-your-key-here" \
  -F "model=doubao/doubao-seedance-2-0-260128" \
  -F "file=@./photo.jpg"
# → { "assetUrl": "https://..." }

# Step 2: create the task with that URL
curl https://oss.chatgpttech.mobi/v1/generation/tasks \
  -H "Authorization: Bearer sk-hub-your-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "doubao/doubao-seedance-2-0-260128",
    "content": [
      {"type": "text", "text": "Make the scene come alive"},
      {"type": "image_url", "image_url": {"url": "ASSET_URL_FROM_STEP_1"}}
    ],
    "ratio": "16:9",
    "resolution": "1080p",
    "duration": 5
  }'
import { readFileSync } from 'node:fs';

// Step 1: upload the image
const form = new FormData();
form.append('model', 'doubao/doubao-seedance-2-0-260128');
form.append('file', new Blob([readFileSync('./photo.jpg')]), 'photo.jpg');

const { assetUrl } = await fetch('https://oss.chatgpttech.mobi/v1/assets/upload', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer sk-hub-your-key-here' },
  body: form,
}).then(r => r.json());

// Step 2: create the task with that URL
const task = await fetch('https://oss.chatgpttech.mobi/v1/generation/tasks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk-hub-your-key-here',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'doubao/doubao-seedance-2-0-260128',
    content: [
      { type: 'text', text: 'Make the scene come alive' },
      { type: 'image_url', image_url: { url: assetUrl } },
    ],
    ratio: '16:9',
    resolution: '1080p',
    duration: 5,
  }),
}).then(r => r.json());

console.log(task.id); // use this to poll, same as text-to-video above
import requests

# Step 1: upload the image
upload = requests.post(
    'https://oss.chatgpttech.mobi/v1/assets/upload',
    headers={'Authorization': 'Bearer sk-hub-your-key-here'},
    data={'model': 'doubao/doubao-seedance-2-0-260128'},
    files={'file': open('./photo.jpg', 'rb')},
)
asset_url = upload.json()['assetUrl']

# Step 2: create the task with that URL
res = requests.post(
    'https://oss.chatgpttech.mobi/v1/generation/tasks',
    headers={'Authorization': 'Bearer sk-hub-your-key-here'},
    json={
        'model': 'doubao/doubao-seedance-2-0-260128',
        'content': [
            {'type': 'text', 'text': 'Make the scene come alive'},
            {'type': 'image_url', 'image_url': {'url': asset_url}},
        ],
        'ratio': '16:9',
        'resolution': '1080p',
        'duration': 5,
    }
)
task = res.json()
print(task['id'])  # use this to poll, same as text-to-video above
Accepted file types: image/png, image/jpeg, image/webp (max 10 MB) and video/mp4 (max 50 MB, for video-editing tasks).

Option 2: Use a public image URL

If the image is already hosted somewhere reachable, skip the upload step entirely and pass the URL directly:
cURL
curl https://oss.chatgpttech.mobi/v1/generation/tasks \
  -H "Authorization: Bearer sk-hub-your-key-here" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "doubao/doubao-seedance-2-0-260128",
    "content": [
      {"type": "text", "text": "Make the scene come alive"},
      {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
    ],
    "ratio": "16:9",
    "resolution": "1080p",
    "duration": 5
  }'

Option 3: Inline base64 (quick tests only)

For a one-off test without wiring up an upload step, you can inline the image directly as a base64 data URI:
import { readFileSync } from 'node:fs';

const imageBuffer = readFileSync('./photo.jpg');
const imageUrl = `data:image/jpeg;base64,${imageBuffer.toString('base64')}`;

const task = await fetch('https://oss.chatgpttech.mobi/v1/generation/tasks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk-hub-your-key-here',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'doubao/doubao-seedance-2-0-260128',
    content: [
      { type: 'text', text: 'Make the scene come alive' },
      { type: 'image_url', image_url: { url: imageUrl } },
    ],
    ratio: '16:9',
    resolution: '1080p',
    duration: 5,
  }),
}).then(r => r.json());

console.log(task.id);
import base64, requests

with open('./photo.jpg', 'rb') as f:
    image_url = f"data:image/jpeg;base64,{base64.b64encode(f.read()).decode()}"

res = requests.post(
    'https://oss.chatgpttech.mobi/v1/generation/tasks',
    headers={'Authorization': 'Bearer sk-hub-your-key-here'},
    json={
        'model': 'doubao/doubao-seedance-2-0-260128',
        'content': [
            {'type': 'text', 'text': 'Make the scene come alive'},
            {'type': 'image_url', 'image_url': {'url': image_url}},
        ],
        'ratio': '16:9',
        'resolution': '1080p',
        'duration': 5,
    }
)
task = res.json()
print(task['id'])
Base64 data URIs count toward the request body size. Large source photos (multi-megapixel phone camera shots) can produce multi-megabyte payloads — downscale to around 1024px on the long edge before encoding, or use Option 1 instead, if you hit a 413 Request Entity Too Large.
Whichever option you use, poll /v1/generation/tasks/{id} exactly as shown above — the response shape and polling logic are identical regardless of how the image was supplied.

Request parameters

ParameterTypeRequiredDescription
modelstringModel ID
contentarrayPrompt array. Each item has type of text or image_url
ratiostringAspect ratio: 16:9 (default), 9:16, 1:1, 4:3, 3:4
durationnumberDuration in seconds: 5 (default), max 60
resolutionstringOutput resolution: 480p, 720p, or 1080p (default)
Note: Video generation models are not available through /v1/chat/completions. Use /v1/generation/tasks for all video generation requests.

Quickstart

API keys

Text models