Sume video content 409: job_not_completed vs job_failed in Python

A 409 from /v1/videos/{id}/content means two things. job_not_completed is retryable, job_failed is not. Here is a Python handler that tells them apart.

4 min readSume
All posts

GET /v1/videos/{id}/content returns 409 in two cases. job_not_completed means the job is still running and you should poll again, while job_failed means the job ended in failure and will never produce a file, so you should stop and read the error.

Treating both as the same is a bug. A client that retries job_failed forever waits for a file that does not exist.

The two 409 codes

The docs are explicit about the difference. job_not_completed advertises retryable: true and a poll_status next action. job_failed advertises retryable: false and next_action: inspect_events, with the same public message that the poll response shows in error.

Sume made job_failed a separate code on purpose, because the retryable code would send clients into an endless poll.

Content endpoint errors on Sume, read 2026-10-05
HTTPCodeMeaningWhat to do
409job_not_completedjob still runningkeep polling the job, then retry
409job_failedterminal failurestop, read error on the poll response
404job_not_foundunknown or foreign jobcheck the id and the key
401unauthorizedmissing or invalid keycheck SUME_API_KEY

The handler

The function below returns the redirect location on success, returns None while the job is running, and raises on a failure. It reads the error body, which holds the code, and does not follow the redirect so you can see the artifact URL.

import json, os, urllib.error, urllib.request

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *a, **k):
        return None

opener = urllib.request.build_opener(NoRedirect)

def content_url(job_id):
    req = urllib.request.Request(
        "https://api.sume.com/v1/videos/" + job_id + "/content?index=0",
        headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"]})
    try:
        opener.open(req, timeout=30)
    except urllib.error.HTTPError as e:
        if e.code in (301, 302, 303, 307, 308):
            return e.headers["Location"]
        body = e.read().decode()
        if e.code == 409 and "job_not_completed" in body:
            return None
        raise RuntimeError(str(e.code) + " " + body)
    return None

How to use it

Call it after your poll loop sees completed, and the result should be a URL. If you call it early, you get None and can wait. If it raises, log the message and fetch the job to read error, since the same public text is there.

The poll loop in the Python polling post is the better place to wait. The content endpoint is for the final fetch.

Common mistakes

Matching on the HTTP status alone loses the distinction, so read the code from the body. Retrying on every 409 hides failures, and so does swallowing the error text.

  • Match the code string, not just 409.
  • Do not retry job_failed.
  • Log the job id with the error.
  • Read the error text on the poll response; it is the public reason for the failure.

Related posts

More in Developers

All Developers posts

Written by Sume