Make Your First Seedance 2.0 API Call

Start with a PiAPI account and API key. To create a Seedance 2.0 video task, send a JSON request to POST https://api.piapi.ai/api/v1/task, retain data.task_id, and poll GET /api/v1/task/{task_id}. The current Seedance examples return a finished video URL at data.output.video when data.status is completed.
This walkthrough uses Python and a text-only prompt. It starts with the current seedance-2 task type and explicit generation settings, then adds error handling and a way to resume polling without submitting another generation.
Verification, September 7, 2026: the request matches PiAPI's published Seedance request schema; response handling was checked against documented examples using offline fixtures. Not live-tested: no generation API key was available for this article, so no generation request was submitted and no video result is claimed.
Prepare Python and your PiAPI API key
Use Python 3.10 or newer. Create a virtual environment and install the HTTP client:
python3 -m venv .venv
.venv/bin/python -m pip install requestsSign in to the PiAPI dashboard and create an API key. Make it available to your process as PIAPI_API_KEY, using your environment or secret store. The program reads it at runtime and sends it in the X-API-Key header. The PiAPI Quickstart covers account and key setup.
The first call below needs no image, video, or audio reference URL. A short prompt describing a ceramic cup and rising steam is enough to demonstrate the request structure. You can replace the prompt when running the file.
Select the task type and mode
PiAPI's Seedance 2 API reference uses three different selectors. model identifies the integration, task_type selects the model variant, and input.mode determines how the prompt and references are used.
| Setting | This example | Meaning |
|---|---|---|
model | seedance | Seedance integration. |
task_type | seedance-2 | Current Seedance 2 task type. |
input.mode | text_to_video | Text-only generation, with no reference inputs. |
input.duration | 5 | Requested output duration in seconds. |
input.resolution | 720p | Explicit requested resolution. |
input.aspect_ratio | 16:9 | Requested frame proportions. |
input.audio | false | Request no generated audio. |
config.service_mode | public | Explicit public service mode. |
The current request schema permits integer durations from 4 to 15 seconds and prompts up to 4,000 characters. It requires mode, even though the prose describes automatic mode inference, so the example always supplies it. Resolution is also explicit because the documented default is 480p.
Use current task names in a new integration. Older seedance-2-preview names still appear in some historical examples, but the current reference lists them as deprecated. Requested settings describe the submitted job; they are not measurements of an output from this article.
Submit the task once
The following three Python blocks form one complete file, example.py. The first builds the request and checks the create response before returning a task ID.
import argparse
import math
import os
import sys
import time
from urllib.parse import quote, urlsplit
import requests
API_BASE = "https://api.piapi.ai/api/v1"
DEFAULT_PROMPT = (
"A ceramic cup on a wooden table beside a window. "
"Steam rises gently while the camera stays still."
)
def request_payload(prompt):
if not isinstance(prompt, str) or not prompt.strip() or len(prompt) > 4000:
raise ValueError("Provide a non-empty prompt of at most 4000 characters")
return {
"model": "seedance",
"task_type": "seedance-2",
"input": {
"prompt": prompt,
"mode": "text_to_video",
"duration": 5,
"resolution": "720p",
"aspect_ratio": "16:9",
"audio": False,
},
"config": {"service_mode": "public"},
}
def response_data(response):
if response.status_code != 200:
raise RuntimeError(f"PiAPI returned HTTP {response.status_code}")
try:
body = response.json()
except ValueError:
raise RuntimeError("PiAPI returned a non-JSON response") from None
if not isinstance(body, dict):
raise RuntimeError("PiAPI returned an unexpected response envelope")
# The published get-task example has timestamp + data, without an outer code.
if "code" in body and body["code"] != 200:
raise RuntimeError("PiAPI returned a non-success API code")
if not isinstance(body.get("data"), dict):
raise RuntimeError("PiAPI response is missing the task data object")
return body["data"]
def create_task(client, prompt):
payload = request_payload(prompt)
try:
response = client.post(
f"{API_BASE}/task", json=payload, timeout=(10, 30), allow_redirects=False
)
except requests.RequestException:
raise RuntimeError(
"Submission was not confirmed. Check task history before creating another task."
) from None
data = response_data(response)
task_id = data.get("task_id")
if not isinstance(task_id, str) or not task_id.strip():
raise RuntimeError("Create response has no task_id; check task history before resubmitting")
return task_idA task ID means you have a reference to submitted work. It does not mean the video is finished. Retain that ID before waiting for the result; in an application, store it with the user's job record.
If the connection breaks during creation, the request may already have reached PiAPI. The function reports an uncertain submission and does not retry POST. Check task history before creating another job after that error or a response without an ID.
Poll until a video URL is available
Add the next block. It waits between queries, handles known task states, and requires a usable video URL before reporting completion.
def wait_for_video(client, task_id, max_wait=600, poll_interval=5):
if not all(math.isfinite(value) and value > 0 for value in (max_wait, poll_interval)):
raise ValueError("Polling budget and interval must be finite and positive")
deadline = time.monotonic() + max_wait
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Polling budget expired; the remote task may still be running")
try:
response = client.get(
f"{API_BASE}/task/{quote(task_id, safe='')}",
timeout=(min(10, remaining), min(30, remaining)),
allow_redirects=False,
)
except requests.RequestException:
raise RuntimeError("Polling request interrupted; resume with the saved task ID") from None
data = response_data(response)
status = data.get("status")
if status == "completed":
output = data.get("output")
video = output.get("video") if isinstance(output, dict) else None
if not isinstance(video, str) or not video.strip():
raise RuntimeError("Completed task is missing data.output.video")
parsed = urlsplit(video)
if parsed.scheme not in {"https", "http"} or not parsed.netloc:
raise RuntimeError("Task returned an invalid video URL")
return video
if status == "failed":
error = data.get("error")
code = error.get("code") if isinstance(error, dict) else None
code = code if isinstance(code, int) else "unavailable"
raise RuntimeError(f"Seedance task failed; API error code: {code}")
if status not in {"pending", "processing"}:
raise RuntimeError("Task returned an unexpected status; inspect the saved task")
time.sleep(min(poll_interval, max(0, deadline - time.monotonic())))pending and processing keep the loop running. failed ends it, while completed must include data.output.video. An unknown state stops for inspection instead of being assumed successful.
The ten-minute budget and five-second polling interval are local example settings, not estimates of generation time. The budget stops new polls after its deadline. The connection/read timeouts in requests do not impose a strict total wall-clock limit on an in-flight request.
Neither a local timeout nor an interrupted poll cancels the remote task. Use the saved ID to query the same task later. The current create reference, the Seedance Get Task example, and the product quickstart agree on completed with output.video; the get-task page's separate machine schema still contains legacy image-task fields. This example follows the Seedance response examples, without claiming that inconsistent schema was fully validated.
Run the example or resume polling
Finish the file with this entry point:
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--prompt", default=DEFAULT_PROMPT)
parser.add_argument("--task-id", help="Query an existing task without creating another")
parser.add_argument("--max-wait", type=float, default=600)
args = parser.parse_args()
key = os.environ.get("PIAPI_API_KEY")
if not key:
parser.error("Set PIAPI_API_KEY in your environment")
if not math.isfinite(args.max_wait) or args.max_wait <= 0:
parser.error("--max-wait must be finite and positive")
with requests.Session() as client:
client.headers.update({"X-API-Key": key, "Content-Type": "application/json"})
task_id = args.task_id or create_task(client, args.prompt)
print(f"Task ID: {task_id}", flush=True)
print(wait_for_video(client, task_id, max_wait=args.max_wait))
if __name__ == "__main__":
try:
main()
except (RuntimeError, ValueError, TimeoutError) as error:
print(str(error), file=sys.stderr)
raise SystemExit(1)With PIAPI_API_KEY set, run the default prompt:
.venv/bin/python example.pyThe program prints the returned task ID first. It prints a video URL only after observing the documented completed response. You can choose another prompt with --prompt.
To resume, replace YOUR_SAVED_TASK_ID with your existing ID:
.venv/bin/python example.py --task-id YOUR_SAVED_TASK_ID --max-wait 600This command only queries the existing task. It does not create a new video request. Keep task IDs in durable storage when integrating the same flow into a service.
Diagnose an unsuccessful call
| Symptom | What to check |
|---|---|
| Authentication or HTTP error | Confirm the API key, endpoint, and account access. |
| Input rejected | Check the current task name, explicit mode, prompt length, duration, and resolution. |
| No task ID or interrupted creation | Inspect task history before submitting again. |
Task reaches failed | Use its task ID and API error code to investigate. |
| Polling is interrupted or its budget expires | Query the same task ID again to inspect its current state. |
| Completed task lacks a video URL | Check the documented output shape before treating the job as consumable. |
The script reports status codes and bounded error messages. It avoids printing request headers, raw upstream exceptions, and entire response bodies that may contain private inputs. A larger application can add structured job records and its own retry policy while preserving the distinction between creating work and retrieving its state.
Frequently asked questions
Can I add images after the first text-only call?
Yes, through the documented reference modes. first_last_frames uses starting or ending images; omni_reference uses supported reference combinations. Change the mode and payload together, following the version-specific reference.
Does an API key replace reference-asset review?
No. API authorization and reference-asset processing are separate parts of the workflow. For managed assets, the Private Asset Library guide documents the required asset state before a reference can be used. This text-only example does not exercise that workflow.
Why does creation return before the video is ready?
Seedance generation is asynchronous. The create response identifies the task; later get-task responses describe its progress and result. Check the status and output together.
Where do I go after the first task?
Use the Seedance 2.0 product page for current integration options and the broader Seedance API guide for additional examples. Keep the current request reference beside your code when changing task types or input modes.



