Polling a Seedance task on Volcengine Ark comes down to four facts. The query endpoint is GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{id}. The status field is named status and carries six values. The finished video sits at content.video_url. And that URL is valid for 24 hours. The API overview covers the create side; everything below is the loop that follows it.
The fields a poll actually returns
A create call hands back a task id and little else. The vendor documents twenty-two response fields; six of them decide what your worker does next.
| Field | What it tells your code |
|---|---|
status | Whether the task is moving, finished, or dead |
content.video_url | The hosted video, present once it succeeds |
content.last_frame_url | The closing frame, same 24-hour window |
error | Why a failed task failed |
execution_expires_after | The task timeout threshold, in seconds |
usage | Token consumption, billed on success |
The vendor’s own example is a 2.0 record, not a 2.5 one: "model": "doubao-seedance-2-0-260128", "resolution": "720p", "duration": 5, "usage": { "completion_tokens": 108900, "total_tokens": 108900 }. Read that token count as a 2.0 shape.
The endpoint and the status field
The query call is a GET against the collection you created into, with the task id as the required path parameter. Authentication is the same long-lived Ark key: Authorization: Bearer $ARK_API_KEY.
The six status values
status is a string with six documented values. The vendor’s Chinese gloss for each is worth keeping beside your branch statement:
queued— 排队中running— 任务运行中cancelled— 取消任务,取消状态 24h 自动删除succeeded— 任务成功failed— 任务失败expired
Two of them change how the loop ends: cancelled is reachable only from queued and the record deletes itself after 24 hours, while expired means the task outlived execution_expires_after, documented default 172800 seconds. Note what is missing: no pending state, so a loop waiting on pending or processing waits forever. The endpoint reference has the request side in full.
Writing the polling loop

The create call that starts the loop:
curl -X POST https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ARK_API_KEY" \
-d '{
"model": "doubao-seedance-2-5-260628",
"content": [
{ "type": "text", "text": "A workshop at dawn, hands assembling a clock movement." }
],
"ratio": "16:9",
"duration": 5
}'
A loop in bash
TASK_ID="your_task_id"
while :; do
payload=$(curl -s -X GET \
"https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/${TASK_ID}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ARK_API_KEY")
status=$(printf '%s' "$payload" | jq -r .status)
case "$status" in
queued|running) sleep 10 ;;
succeeded)
curl -sL -o "seedance-${TASK_ID}.mp4" \
"$(printf '%s' "$payload" | jq -r .content.video_url)"
break ;;
failed|expired|cancelled)
printf '%s' "$payload" | jq -r .error
break ;;
*) echo "unexpected status: $status"; break ;;
esac
done
The case has no catch-all sleep, so an unknown string ends the loop instead of spinning on it. That matters more than the interval: the vendor publishes no recommended polling interval.
The same loop with the official SDK
Ark ships an official Python SDK under the package name volcenginesdkarkruntime.
import os
import time
from volcenginesdkarkruntime import Ark
client = Ark(api_key=os.environ["ARK_API_KEY"])
task = client.content_generation.tasks.create(
model="doubao-seedance-2-5-260628",
content=[{"type": "text", "text": "A workshop at dawn."}],
ratio="16:9",
duration=5,
)
print(task.id)
TERMINAL = {"succeeded", "failed", "expired", "cancelled"}
while True:
result = client.content_generation.tasks.get(task_id=task.id)
if result.status in TERMINAL:
break
time.sleep(10)
print(result.status)
print(getattr(result.content, "video_url", None))
Cancelling or deleting a task
The same six values decide whether a DELETE is legal. DELETE https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{id} does two different jobs and refuses two states outright.
| Current status | DELETE supported | What it does | Status after |
|---|---|---|---|
queued | Yes | Cancels the queue entry | cancelled |
running | No | — | — |
succeeded | Yes | Deletes the record; it can no longer be queried | — |
failed | Yes | Deletes the record; it can no longer be queried | — |
cancelled | No | — | — |
expired | Yes | Deletes the record; it can no longer be queried | — |
A successful DELETE returns HTTP 200 with an empty object, {}. A running task cannot be stopped, so a cancel button has to accept that it may do nothing.
A list call at the same path takes page_num and page_size — defaulting to 1 and 20, permitted in [1, 500] — plus filter.status, filter.task_ids and filter.model. Reconcile against that list rather than trusting your own ledger alone.
The 24-hour window on the result URL
Three windows, published separately, and they do not line up.
| Window | Value |
|---|---|
| Task records stay queryable | 最近 7 天 |
| Result URL validity | 24 小时 |
| Downloads on a 2.5 result URL | 上限 100 次 |
The vendor’s wording is precise. Records: 仅支持查询最近 7 天的任务记录,时间区间为 [T-7 天, T),其中 T 为请求发起时刻的 UTC 时间戳(精确到秒)。 URL: 视频 URL 有效期为 24 小时,请及时下载或转存。 Downloads: Seedance 2.5 模型生成的视频 URL 下载次数上限为 100 次。 The last frame image is on the same clock.
What to do before the URL expires
Poll once, then copy. Do not hand the Ark URL to a user, because the hundredth download ends it for everyone.
import os
import requests
from volcenginesdkarkruntime import Ark
client = Ark(api_key=os.environ["ARK_API_KEY"])
result = client.content_generation.tasks.get(task_id="your_task_id")
if result.status == "succeeded":
response = requests.get(result.content.video_url, timeout=120)
response.raise_for_status()
with open(f"seedance-{result.id}.mp4", "wb") as handle:
handle.write(response.content)
print("archived", len(response.content), "bytes")
That is the whole discipline: one download, into your own storage, immediately. Downloading on every poll burns the allowance and the clock together. If the token side is what you are modelling, the price breakdown has the formula.
What the vendor has not published
Not published as of this review:
- A recommended polling interval or a backoff policy. The vendor says polling works and that webhooks are an alternative, and stops there.
- The HTTP status code and response headers used when a request is throttled. No
429and noRetry-Afterappears in any official page reviewed here. - Any request tracing header, so the task id is all you get for correlating a poll with a support ticket.
- Task duration ranges, queue depth, latency and success-rate guarantees.
- Any free quota for this model.
The vendor’s example shows an id shaped like cgt-2026****-****, but that shape is never stated as a rule. Branch on status, not on the id prefix.
Frequently asked questions
Which endpoint polls a task? GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{id}, with the task id as the path parameter and the Ark key in the Authorization header.
What are the six status values? queued, running, cancelled, succeeded, failed and expired. There is no pending or processing state, and a loop waiting on those waits forever.
How long do I have to fetch the video? 24 hours, with at most 100 downloads on a Seedance 2.5 result URL. Task records are queryable for 7 days.
Can I stop a running task? No. Only a queued task can be cancelled; running and cancelled both reject DELETE.
Can I avoid polling entirely? The vendor documents a callback mechanism as an alternative, but the field-level detail was not reachable during this review, so treat that route as documented but unverified.
Check every field and every window against the official pages before shipping; both the rate limits and the expiry rules move without much notice.