Video job statuses: pending, cancelled vs Sume's five job states
A /v1/videos job says pending or in_progress; /v1/jobs/{id}/status says queued or processing. Here is the mapping, which one to poll, and the spelling trap.

The same Sume video job has two vocabularies. Read it at GET /v1/videos/{id} and you get pending, in_progress, completed, failed or cancelled. Read it at GET /v1/jobs/{id}/status and you get queued, processing, completed, failed or canceled. Both describe one job id, and the two never disagree.
Why there are two
The /v1/videos route is a copy of the OpenRouter video generation wire, so that a client written from their guide works after a base-URL change. OpenRouter's guide lists pending, in_progress, completed and failed, and its webhook events add cancelled and expired. Sume keeps that vocabulary on this route and maps its own five internal states onto it.
The generic jobs routes use Sume's own envelope. They exist for every generation product, not only video, and they carry extras such as terminal and result_ready booleans and a next-poll hint that the OpenRouter shape has no place for.
The mapping is also a compatibility promise. Sume's own contract for this route says the OpenRouter shape wins when the two disagree, apart from a short published list of differences. Status names are not on that list, so you can rely on the OpenRouter vocabulary here and on the Sume vocabulary on the jobs routes.
The mapping
Sume does not expire jobs. The expired value is in the enum for wire compatibility and the API never emits it, so you can leave it out of a switch, or handle it as a no-op.
| Sume jobs route | /v1/videos route | Terminal | What to do |
|---|---|---|---|
| queued | pending | No | Keep polling |
| processing | in_progress | No | Keep polling |
| completed | completed | Yes | Fetch content |
| failed | failed | Yes | Read the error field |
| canceled | cancelled | Yes | Stop |
A normalizer you can paste
If your code touches both routes, for example a poll on /v1/videos and a dashboard that reads /v1/jobs, normalize once at the edge.
TERMINAL = {"completed", "failed", "canceled"}
ALIASES = {
"pending": "queued",
"in_progress": "processing",
"cancelled": "canceled",
"expired": "failed", # never emitted by Sume, kept for wire parity
}
def normalize(status: str) -> str:
return ALIASES.get(status, status)
def is_terminal(status: str) -> bool:
return normalize(status) in TERMINAL
assert normalize("pending") == "queued"
assert normalize("cancelled") == "canceled"
assert is_terminal("cancelled") and not is_terminal("in_progress")
print("ok")
The spelling trap
The most common bug is a single letter. A check for the string canceled on the video route never matches, because that route says cancelled, so the loop keeps polling a job that ended. The reverse bug happens when a handler written for the video route reads the jobs route. Normalizing, as above, removes the class of error.
Tie the normalizer to your tests with the five rows from the table. The assertions in the snippet already do that, and they run with plain python.
If you build a status badge in a UI, show your own words and map from the normalized value. Users do not care whether the wire said pending or queued, and a badge keyed on the normalized set survives a future route that adds a third spelling. Keep the raw wire value in your debug panel, since support will ask for it.
Which route to poll
For a new video integration, poll /v1/videos/{id} because the submit response already hands you the polling_url. Use /v1/jobs/{id}/status when you want terminal, result_ready and next_poll_after_seconds, or when one dashboard follows image, video and avatar jobs together. Cancel works on the jobs route only before generation starts, and then it returns 409 job_generation_already_started.
- Do not mix the two names in one enum.
- Treat unknown future values as non-terminal, and log them.
- A failed job explains itself in the error field, not in the HTTP status of the poll.
Sources
Related posts
More in Developers
- Video model fallback chain in Python with one key per model
A Python chain that tries Gemini Omni, Wan 3.0 and Seedance 2 on Sume in order, stops on 401 or 402, and uses a separate idempotency key for each model.
- 'Video Router generate requires a catalog model id': the fix
The model must be an id from the catalog or an Auto alias. Provider names, vendor slugs and marketing names fail. List valid ids from /v1/videos/models first.
- wait_timeout_seconds 30 is not a 30-second video
The 30 in wait_timeout_seconds is how long your HTTP request may block, not how long a clip may run. A 30-second video job still needs a poll or webhook.
- waitForJob timeout in the Sume TypeScript SDK: keep the job id
waitForJob waits 20 minutes by default and throws SumeJobTimeoutError without cancelling the render. Catch it, store jobId, and resume later. Sume SDK 0.2.0.
Written by Sume