fishtoken Sign up
GuidePythonVideo

Generate a video with Seedance 2.0 Fast in Python

A complete script for doubao-seedance-2-0-fast-260128: submit a generation to /v1/video/generations, poll the task until it is ready, save the result URL and download the file. Then a few notes on writing prompts for Seedance and on cost.

Before you run it

  1. Account, balance, key

    Sign up at fishtoken.ai/register, add balance on the top-up page (FishToken is prepaid; the balance does not expire), and create a key under API keys.

  2. Install one package

    pip install requests. The OpenAI SDK is not needed here because video generation has its own endpoint.

  3. Export the key

    export FISHTOKEN_API_KEY=your-key-here. The script reads it from the environment and never has it in the source.

The script

Save as seedance_fast.py. It has three steps: submit() posts the request, wait_for() polls the task endpoint, video_url() pulls the URL out of the finished task.

seedance_fast.py
"""Generate a video with Seedance 2.0 Fast through FishToken, wait for it, save the result URL.

    pip install requests
    export FISHTOKEN_API_KEY=your-key-here
    python3 seedance_fast.py
"""
import os
import sys
import time

import requests

API = "https://fishtoken.ai/v1"
MODEL = "doubao-seedance-2-0-fast-260128"
HEADERS = {"Authorization": f"Bearer {os.environ['FISHTOKEN_API_KEY']}"}
PROMPT = ("Vertical shot: a woven rattan handbag turning slowly on a sunlit wooden table, "
          "soft shadows, shallow depth of field")

POLL_EVERY = 5      # seconds between status checks
MAX_WAIT = 10 * 60  # give up after ten minutes


def submit(prompt: str) -> str:
    """Submit a generation task and return its task ID."""
    r = requests.post(f"{API}/video/generations", headers=HEADERS,
                      json={"model": MODEL, "prompt": prompt}, timeout=60)
    r.raise_for_status()
    data = r.json()
    # PLACEHOLDER: the field that holds the task ID is defined in https://fishtoken.ai/docs
    # (commonly "id" or "task_id"). Print data once and adjust the line below if needed.
    task_id = data.get("id") or data.get("task_id")
    if not task_id:
        sys.exit(f"no task ID in response: {data}")
    return task_id


def wait_for(task_id: str) -> dict:
    """Poll the task until it finishes or MAX_WAIT passes."""
    deadline = time.time() + MAX_WAIT
    while time.time() < deadline:
        r = requests.get(f"{API}/video/generations/{task_id}", headers=HEADERS, timeout=60)
        r.raise_for_status()
        task = r.json()
        # PLACEHOLDER: check the task endpoint in https://fishtoken.ai/docs for the exact
        # status values and the field that carries the video URL, then adjust these two lines.
        status = str(task.get("status", "")).lower()
        if status in ("succeeded", "completed", "success", "done"):
            return task
        if status in ("failed", "error", "cancelled"):
            sys.exit(f"task {task_id} failed: {task}")
        print(f"  {status or 'pending'} ... checking again in {POLL_EVERY}s")
        time.sleep(POLL_EVERY)
    sys.exit(f"task {task_id} not finished after {MAX_WAIT}s")


def video_url(task: dict) -> str:
    """Find the first http(s) URL in the finished task, whatever key it sits under."""
    stack = [task]
    while stack:
        item = stack.pop()
        if isinstance(item, dict):
            stack.extend(item.values())
        elif isinstance(item, list):
            stack.extend(item)
        elif isinstance(item, str) and item.startswith("http"):
            return item
    sys.exit(f"no URL in finished task: {task}")


if __name__ == "__main__":
    task_id = submit(PROMPT)
    print(f"submitted task {task_id}")
    finished = wait_for(task_id)
    url = video_url(finished)
    with open("seedance_result.txt", "a", encoding="utf8") as f:
        f.write(f"{task_id}\t{url}\n")
    print(f"video ready: {url}")
    print("saved to seedance_result.txt")

    # Optional: download the file. Result URLs may expire, so do this soon after the task finishes.
    mp4 = requests.get(url, timeout=300)
    mp4.raise_for_status()
    with open(f"{task_id}.mp4", "wb") as f:
        f.write(mp4.content)
    print(f"downloaded {task_id}.mp4 ({len(mp4.content) // 1024} KB)")

Two placeholders. The exact names of the task-ID field, the status values and the URL field are defined by the FishToken docs. The script guesses the common names and otherwise prints the raw response so you can see what to adjust; the comments marked PLACEHOLDER show where. Other fields Seedance accepts, such as duration or size, go next to prompt in submit().

Expected output
$ python3 seedance_fast.py
submitted task TASK_ID
  pending ... checking again in 5s
  pending ... checking again in 5s
video ready: https://.../TASK_ID.mp4
saved to seedance_result.txt
downloaded TASK_ID.mp4 (... KB)

Writing prompts for Seedance

One shot per clip

Describe a single scene with one subject and one camera move. Cuts and scene changes inside one prompt tend to produce muddled results; generate two clips and edit them together instead.

Name the subject, the motion, the light

“A rattan handbag turning slowly on a wooden table, sunlight from the left, soft shadows” gives the model something concrete. Adjectives alone (“beautiful”, “cinematic”) do little.

Say the orientation

Start the prompt with “Vertical shot” for short-video and social formats, or “Wide shot” for landscape, and check the size options in the docs if the model takes an explicit size field.

Draft on Fast, then rerun

Iterate on wording with doubao-seedance-2-0-fast-260128. When a prompt works, run the same text on doubao-seedance-2-0-260128 or doubao-seedance-2-5-260628 by changing MODEL; the key and balance are the same.

Keep it within the rules

No real person's likeness, no brand logos you do not own, nothing sexual or violent. Requests like that may be refused by the upstream model service and are against the content rules either way.

Log what you tried

The script appends every task ID and URL to seedance_result.txt. Add the prompt to that line too, and you have a record of which wording produced which clip.

What it costs

Each generation is charged to your prepaid balance when the task runs; polling the task endpoint is not a generation. Video models are billed per request or by usage depending on the model, and the current price for each Seedance ID is on fishtoken.ai/pricing. Check it before a batch job, and remember that prices on that page are shown in platform credits.

Rate limits

60 requests per minute for standard accounts and 120 for accounts that have topped up. The poll interval of five seconds in the script stays well inside that even with several tasks running; if you parallelise, add a back-off on 429 responses.

Before you publish

Content filtering depends on the upstream model service. You are responsible for what you generate and publish: no sexual content, no graphic violence, and no real person's likeness or voice without their consent. Label AI-generated content where the platform you publish on or local law requires it.

Read the full content rules

Generate your first clip

Create an account, add balance, create a key and run the script.