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.

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.
| Code | Job state | Retryable | What to do |
|---|---|---|---|
| job_not_completed | pending or in_progress | Yes | Poll status with backoff, then fetch content |
| job_failed | failed | No | Read events, fix input, submit a new job |
| (200 redirect) | completed | n/a | Follow the redirect to the artifact; index defaults to 0 |
| job_not_found (404) | unknown or foreign id | No | Check 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
- One image to Seedance: reference, or first frame?
On Sume a single reference image with no frame field is priced and routed as reference-to-video; add a first frame to get image-to-video. How to choose.
- Seedance size parameter returns 400 on Sume: use resolution
Sending size such as 1280x720 to /v1/videos returns 400 unsupported_parameter on Sume. Use resolution plus aspect_ratio instead.
- Seedance on Video Router or /v1/videos: which endpoint for new code
Sume says new Seedance integrations should use POST /v1/videos. Video Router still works unchanged with the same ids. Here is the field difference.
- Retry a Seedance submit safely: Idempotency-Key on /v1/videos
A timed-out POST to /v1/videos can create two paid jobs. Send an Idempotency-Key and a replay returns the original job. Python example included.
Written by Sume