
This guide shows the smallest request that produces a video with seedance-2.0-fast, the full list of input fields the model actually accepts, and how to wait for the result with polling or a callback. Every request shape and error message below was run against the live API, so you can copy the code as-is.
One goal: send a text prompt, get back a downloadable MP4 URL. seedance-2.0-fast is the speed-optimized variant of Seedance 2.0 — it trades top-end resolution (it caps at 720p) for faster turnaround, which makes it the right pick for previews, iteration loops, and high-volume short clips.
You need exactly one thing: a hiapi API key. Grab one from your dashboard — it looks like sk-.... All video models, including this one, are served through the unified async task endpoint: you POST /v1/tasks to create a job, then fetch the result by task id. There is no synchronous mode for video.
Current per-clip pricing is on the pricing page (the model was just added, so check there rather than trusting any number you find in a blog post).
Create the task:
curl -X POST "https://api.hiapi.ai/v1/tasks" \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.0-fast",
"input": {
"prompt": "A paper boat drifts down a rain-soaked street at dusk, cinematic, slow dolly-in",
"duration": 5,
"resolution": "720p",
"aspect_ratio": "16:9"
}
}'
The response contains a task id under data.taskId:
{
"data": {
"taskId": "task_abc123..."
}
}
Poll it until the status turns terminal:
curl "https://api.hiapi.ai/v1/tasks/task_abc123..." \
-H "Authorization: Bearer sk-YOUR_KEY"
When data.status is "success", the video URL is at data.output[0].url. The URL is signed and expires — download the file right away instead of hot-linking it:
curl -o clip.mp4 "<the output url>"
Only prompt is required. Anything not in this table is rejected with a 400 — the schema does not allow extra properties.
| Field | Type | Required | Accepted values |
|---|---|---|---|
prompt | string | yes | min length 3 characters |
duration | integer | no | 4–15 (seconds) |
resolution | string | no | 480p, 720p |
aspect_ratio | string | no | 1:1, 4:3, 3:4, 16:9, 9:16, 21:9, adaptive |
Two things worth calling out:
duration is an integer, not a string. Sending "duration": "5" fails validation with duration: got string, want integer.seedance-2.0-fast is text-to-video only; fields like image_urls are rejected by the schema. If you need image-to-video, see the image-to-video API workflow guide for models that support it.No SDK required — requests is enough:
import time
import requests
API_BASE = "https://api.hiapi.ai/v1/tasks"
HEADERS = {
"Authorization": "Bearer sk-YOUR_KEY",
"Content-Type": "application/json",
}
def create_task(prompt: str, duration: int = 5,
resolution: str = "720p", aspect_ratio: str = "16:9") -> str:
payload = {
"model": "seedance-2.0-fast",
"input": {
"prompt": prompt,
"duration": duration,
"resolution": resolution,
"aspect_ratio": aspect_ratio,
},
}
r = requests.post(API_BASE, json=payload, headers=HEADERS, timeout=60)
r.raise_for_status()
return r.json()["data"]["taskId"]
def wait_task(task_id: str, timeout_s: int = 600, poll_interval: int = 5) -> dict:
deadline = time.time() + timeout_s
while time.time() < deadline:
r = requests.get(f"{API_BASE}/{task_id}", headers=HEADERS, timeout=30)
r.raise_for_status()
task = r.json()["data"]
if task["status"] == "success":
return task
if task["status"] == "fail":
err = task.get("error") or {}
raise RuntimeError(f"task failed: {err.get('code')}: {err.get('message')}")
time.sleep(poll_interval)
raise TimeoutError(f"task {task_id} still running after {timeout_s}s")
task_id = create_task(
"A barista pours latte art in a sunlit cafe, shallow depth of field"
)
task = wait_task(task_id)
video_url = task["output"][0]["url"]
# The URL is signed and expires — persist the bytes immediately.
with open("clip.mp4", "wb") as f:
f.write(requests.get(video_url, timeout=120).content)
print("saved clip.mp4")
Treat any status other than success / fail as "still running" — that keeps the loop robust if intermediate status names ever change.
For anything beyond a script, register a callback when you create the task and skip polling entirely:
{
"model": "seedance-2.0-fast",
"input": { "prompt": "..." },
"callback": { "url": "https://your-app.com/hooks/hiapi", "when": "final" }
}
With "when": "final" your endpoint gets exactly one POST when the task reaches a terminal state. Polling is fine for CLIs and notebooks; callbacks win when you run at volume (no wasted requests, no poll-interval latency). If your hook doesn't seem to fire, work through the callback troubleshooting guide — it's almost always URL reachability or a non-2xx response from your handler.
Task creation is not free, so guard against double-submits on your side: key each business event to at most one POST /v1/tasks call (e.g. store taskId against your order/job row before retrying). If your process crashes after creating a task, recover by re-reading the stored taskId and resuming GET /v1/tasks/<id> — the task keeps running server-side whether or not you poll it.
| Symptom | Meaning | Fix |
|---|---|---|
400 with error_code: "INVALID_REQUEST" | Input failed schema validation; the message names the exact field, e.g. duration: maximum: got 20, want 15 | Fix the field; only the four fields in the table above are allowed |
401 with error.code: "permission_denied" | Key is invalid, or valid but not enabled for this model | Check the key in your dashboard; model permissions are per-key |
404 with "task not found" on GET | Wrong or foreign task id | Use the taskId returned at creation, same account |
Status "fail" with error object on the task | Generation itself failed | Read error.code/error.message; retry with an adjusted prompt |
Note the two error envelopes: validation errors put message at the top level, while auth errors nest under error. Handle both if you write generic middleware.
If you need 1080p or 4K output, use the full seedance-2.0 instead — same endpoint, same flow, higher resolution ceiling (and it requires aspect_ratio explicitly). For a broader look at how the Seedance family compares to the other video models on the platform, see the video model comparison. Full parameter references for every model live in the docs, and the model page tracks current capability and pricing status.
What resolutions does seedance-2.0-fast support?
480p and 720p. That's the trade for speed — the full seedance-2.0 goes up to 1080p and 4k on the same /v1/tasks flow.
Can I do image-to-video with seedance-2.0-fast?
No. The input schema accepts only prompt, duration, resolution, and aspect_ratio; image fields are rejected with a validation error. For image-to-video options on hiapi, see the image-to-video workflow guide.
How long can the generated video be?
duration accepts integers from 4 to 15 seconds. Values outside that range fail validation before a task is created (you are not billed for rejected requests).
Should I poll or use a callback?
Polling every ~5 s is fine for scripts and low volume. At production volume, pass callback: {"url": ..., "when": "final"} at creation and let hiapi push the terminal state to you — one request instead of dozens.
Why do I get 401 permission_denied with a key that works for other models?
Model access is scoped per key. A 401 with permission_denied on this model means the key exists but isn't enabled for seedance-2.0-fast — check the key's model permissions in the dashboard.
How long is the output URL valid? It's a signed URL with an expiry. Download the MP4 as soon as the task succeeds and store it yourself; don't embed the raw output URL anywhere long-lived.
Key Takeaways