Seedance or Kling video 409: job_not_completed vs job_failed

A 409 from GET /v1/videos/{id}/content means two opposite things on Sume: job_not_completed is retryable, job_failed is not. How to tell them apart.

5 min readSume
All posts

If GET /v1/videos/{job_id}/content answers 409 for a Seedance or Kling job, read the error code before you do anything else. job_not_completed means the clip is still rendering and the request is retryable. job_failed means the job ended in failure, it is not retryable, and polling the content URL will never produce a file.

Both codes share an HTTP status, which is why a retry loop that only checks status == 409 ends up hammering a dead job forever. This post shows how Sume's video generation API separates them, what each response tells your client to do next, and a download helper that handles both.

What does 409 job_not_completed mean?

It means you asked for the file before the job reached completed. Seedance 2.5 clips run 4 to 30 seconds of output and Kling 3 clips 4 to 15, and neither renders instantly, so a content request made right after submit lands here. Sume documents the response as retryable, with next_action: poll_status.

The fix is not to retry the content endpoint in a tight loop. Poll the job instead, either the OpenRouter-shaped GET /v1/videos/{job_id} or the same job at GET /v1/jobs/{id}/status, and fetch content once the status is completed. The job statuses map as queued to pending, processing to in_progress, and completed, failed, canceled to completed, failed, cancelled. A job that is only waiting for a concurrency slot is pending, which is normal and not an error; see Generation admission.

What does 409 job_failed mean?

It means the job reached a terminal failure. The response carries retryable: false and next_action: inspect_events, plus the same public message you would see in the error field of the poll response. Sume deliberately does not use job_not_completed here, because that code advertises retryable: true and would send a client into an endless poll for content that will never exist.

Read GET /v1/jobs/{id}/events for the timeline, fix the input, and submit a new job. A failed job releases or refunds its reservation where applicable, per the errors page, so you are not paying twice for the same failure. If the message mentions an input URL, the cause is usually a frame or reference image Sume could not fetch.

How do the two responses compare?

Branch on the code, never on the status alone.

GET /v1/videos/{id}/content on a non-completed job, read 2026-10-02
CodeJob stateRetryableWhat to do
job_not_completedpending or in_progressYesPoll status with backoff, then fetch content
job_failedfailedNoRead events, fix input, submit a new job
(200 redirect)completedn/aFollow the redirect to the artifact; index defaults to 0
job_not_found (404)unknown or foreign idNoCheck the id and the API key's workspace

How do I write a download helper that handles both?

This helper treats job_failed as fatal, sleeps on job_not_completed, and gives up at your own deadline. Sume's docs suggest polling roughly every 30 seconds for video, so 15 seconds here is already generous. It uses requests, which follows the redirect to the artifact and drops the Authorization header when the redirect leaves the API host.

import os, time, requests

BASE = "https://api.sume.com/v1/videos"
HEADERS = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def download(job_id: str, path: str, deadline_s: int = 900) -> str:
    end = time.time() + deadline_s
    while time.time() < end:
        r = requests.get(f"{BASE}/{job_id}/content?index=0",
                         headers=HEADERS, timeout=120)
        if r.status_code == 200:
            with open(path, "wb") as f:
                f.write(r.content)
            return path
        if r.status_code == 409:
            if "job_failed" in r.text:
                raise RuntimeError(f"job failed: {r.text}")
            time.sleep(15)  # job_not_completed: still rendering
            continue
        r.raise_for_status()
    raise TimeoutError(f"{job_id} not ready after {deadline_s}s")

if __name__ == "__main__":
    print(download(os.environ["JOB_ID"], "clip.mp4"))

What should I store after the download?

Store the file you downloaded, or the Sume-hosted artifact URL from the job result, not a provider URL. Sume mirrors generated output into its own media URLs before exposing it, and the media inputs page tells integrations to keep the Sume URL. Artifact paths should be treated as opaque.

Two honest limits. Sume does not expose a queue position or ETA for a pending job, so there is no way to compute a precise wait. And the content endpoint is a convenience over the job result: if you already poll GET /v1/jobs/{id}/result, you can skip it and read the artifact there. Either way, the rule from Jobs and results holds: if a wait times out, keep polling the same job id and do not resubmit a paid request.

Should I use webhooks instead of polling?

Sume's communication options include a webhook_url on supported routes, which avoids the polling loop entirely. If you use one, verify the signature with a non-empty secret before trusting the payload, and still keep a poll as a fallback for a missed delivery.

Either way, treat the content endpoint as the last step, after status says completed. That ordering means the 409 job_not_completed path is a safety net rather than the main flow.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume