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.

5 min readSume
All posts

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.

Status names by route (read 2026-10-02)
Meaning/v1/videos/{id}/v1/jobs/{id}/status
Waitingpendingqueued
Runningin_progressprocessing
Donecompletedcompleted
Failedfailedfailed
Stopped by youcancelledcanceled

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 TERMINAL

What 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

All Developers posts

Written by Sume