Seedance job status: pending vs queued, canceled vs cancelled
Sume has two status vocabularies for one video job: pending/in_progress on /v1/videos and queued/processing on /v1/jobs. A poller must handle both.

The same Seedance job reads as pending or in_progress on /v1/videos/{id} but as queued or processing on /v1/jobs/{id}/status. Terminal states are spelled cancelled on the videos route and canceled on the jobs route. A poller that only knows one vocabulary can spin forever on the other.
Mixing the two is a classic source of an endless polling loop, so it is worth a few lines of care.
What does each route call the states?
Both lists come from the Sume docs: the video guide for the first column and the API reference for the second.
| Meaning | /v1/videos/{id} | /v1/jobs/{id}/status |
|---|---|---|
| Waiting | pending | queued |
| Running | in_progress | processing |
| Done | completed | completed |
| Failed | failed | failed |
| Stopped by you | cancelled | canceled |
Which route should I poll?
Either works. The jobs route is described as a lightweight status read for polling, and the result payload is at GET /v1/jobs/:id/result. Pick one per integration and normalize.
Which route you read depends on what you want back. The videos route gives the OpenRouter-shaped response with unsigned_urls once finished. The jobs routes give Sume's own envelope and a separate result call. Both describe the same job, so you can start on one and check on the other while debugging.
How do I normalize in code?
Map every spelling to a small set of your own. This helper treats anything not terminal as still running.
Notice that the helper treats unknown strings as pass-through. That is intentional: if Sume adds a state later, your code should not crash, and is_done will return false so you keep waiting until a timeout you set yourself.
Set that timeout. A reasonable ceiling is a few times the longest generation you expect; the docs say video typically takes 30 seconds to several minutes. When it passes, read the events and the status once more, and only then decide whether to cancel.
TERMINAL = {"completed", "failed", "cancelled", "canceled"}
def normalize(status: str) -> str:
s = status.lower()
if s in ("pending", "queued"):
return "waiting"
if s in ("in_progress", "processing"):
return "running"
if s in ("cancelled", "canceled"):
return "cancelled"
return s
def is_done(status: str) -> bool:
return status.lower() in TERMINALWhat about cancelling?
The API reference says a job can be cancelled before generation starts; after that POST /v1/jobs/:id/cancel returns 409 job_generation_already_started, and cancelling an already-cancelled job is idempotent. See cancelling video jobs. To avoid polling entirely, pass callback_url.
One more spelling trap: if you store statuses in a database enum, add both cancelled and canceled or normalize on write. A single misspelled value is enough to leave a row stuck as running forever.
Sources
Related posts
More in Developers
- One image to Seedance: reference, or first frame?
On Sume a single reference image with no frame field is priced and routed as reference-to-video; add a first frame to get image-to-video. How to choose.
- Seedance size parameter returns 400 on Sume: use resolution
Sending size such as 1280x720 to /v1/videos returns 400 unsupported_parameter on Sume. Use resolution plus aspect_ratio instead.
- Seedance on Video Router or /v1/videos: which endpoint for new code
Sume says new Seedance integrations should use POST /v1/videos. Video Router still works unchanged with the same ids. Here is the field difference.
- Retry a Seedance submit safely: Idempotency-Key on /v1/videos
A timed-out POST to /v1/videos can create two paid jobs. Send an Idempotency-Key and a replay returns the original job. Python example included.
Written by Sume