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.

FieldWhat it tells your code
statusWhether the task is moving, finished, or dead
content.video_urlThe hosted video, present once it succeeds
content.last_frame_urlThe closing frame, same 24-hour window
errorWhy a failed task failed
execution_expires_afterThe task timeout threshold, in seconds
usageToken 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

A state machine for a video generation task: a created node branching to running, then to succeeded or failed, with the result URL marked as expiring on a timer.

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 statusDELETE supportedWhat it doesStatus after
queuedYesCancels the queue entrycancelled
runningNo——
succeededYesDeletes the record; it can no longer be queried—
failedYesDeletes the record; it can no longer be queried—
cancelledNo——
expiredYesDeletes 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.

WindowValue
Task records stay queryable最近 7 天
Result URL validity24 小时
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 429 and no Retry-After appears 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.