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.

4 min readSume
All posts

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.

Status names by route (Sume docs and OpenRouter guide, read 2026-10-05)
Sume jobs route/v1/videos routeTerminalWhat to do
queuedpendingNoKeep polling
processingin_progressNoKeep polling
completedcompletedYesFetch content
failedfailedYesRead the error field
canceledcancelledYesStop

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

All Developers posts

Written by Sume