A /v1/videos id is a Sume job id: poll /v1/jobs for more (Python)

POST /v1/videos returns an id that is also a Sume job id. Poll GET /v1/jobs/{id}/status for terminal and next_poll_after_seconds, and cancel with /v1/jobs.

4 min readSume
All posts

POST /v1/videos answers 202 with a bare OpenRouter-style object: id, polling_url, status and model. It is natural to poll polling_url and stop there. But the id is not a separate video handle. It is the Sume job id, the same one the Jobs API uses, which is why the poll response also repeats it as generation_id.

That means every job route accepts it, and some of them tell you more than the OpenRouter-shaped poll does. The video poll gives you pending, in_progress, completed, failed or cancelled. The job status route gives you a terminal boolean, a result_ready flag and a next_poll_after_seconds hint.

Which route for which job

Routes that accept a /v1/videos id, from the Sume docs (read 2026-10-03)
NeedRoute
Poll in OpenRouter's shapeGET /v1/videos/{id}
Poll with terminal and a wait hintGET /v1/jobs/{id}/status
Download the videoGET /v1/videos/{id}/content?index=0, a 302 redirect
Read the final job record and errorGET /v1/jobs/{id}
Ask for cancellationPOST /v1/jobs/{id}/cancel
Read eventsGET /v1/jobs/{id}/events

Poll through the Jobs API

The sample submits a Recast job through /v1/videos, then loops on /v1/jobs/{id}/status. The status routes wrap their response in data, unlike the video routes. The loop stops on terminal, and sleeps at least 2 seconds or whatever next_poll_after_seconds says. The standard library is enough.

import json, os, time, urllib.request

BASE = "https://api.sume.com/v1"
HEAD = {"x-api-key": os.environ["SUME_API_KEY"], "Content-Type": "application/json"}


def call(path: str, body: dict | None = None) -> dict:
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(BASE + path, data, HEAD)
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.load(r)


video = call("/videos", {
    "model": "h3-max-recast",
    "input_references": [
        {"type": "video_url", "video_url": {"url": "https://example.com/source.mp4"}},
        {"type": "image_url", "image_url": {"url": "https://example.com/host.jpg"}},
    ],
})
job_id = video["id"]  # the same id every /v1/jobs route takes
while True:
    s = call(f"/jobs/{job_id}/status")["data"]
    print(s["sume_status"], s.get("next_poll_after_seconds"))
    if s["terminal"]:
        break
    time.sleep(max(2.0, s.get("next_poll_after_seconds") or 0))
print("result:", s["result_url"])

Why the hint beats a fixed sleep

  • A queued job can wait behind your workspace's concurrency limit. Polling at a fixed 30 seconds is either too slow for a short job or wasteful for a queued one, and the hint tracks the job's own state.
  • terminal is true for completed, failed and canceled. Check sume_status before you fetch the result, and read GET /v1/jobs/{id} for the error when it is not completed.
  • Status reads count against your read budget, which defaults to 40 times the write budget, so a 2-second floor keeps a pool of jobs well inside it.
  • The cancel route only works before generation starts. A running job returns cancelable: false and finishes, so check that flag first.

The status fields and the cancel rule are in the jobs guide. The OpenRouter-shaped routes and their statuses are in the videos docs.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume