Cancel a Sume video job twice: the second call returns the same job
POST /v1/jobs/{id}/cancel is idempotent for a canceled job and returns 409 once generation has started. How to write a cancel that is safe to retry.

A cancel call on a Sume job is safe to repeat. The jobs page says cancelling a job that is already canceled is idempotent and returns the same canceled job. What it does not do is stop a job that has started: once generation has begun, cancel returns 409 job_generation_already_started with details.cancelable: false, and the job completes or fails normally. A cancel that cannot succeed is not an error in your system; it is information.
This matters most for video, where clips are expensive and you may want to drop queued work when a user closes a page.
The three outcomes
From the jobs and admission pages, read 2026-10-03.
| Job state when you call | Result | What to do |
|---|---|---|
| queued | Canceled | Mark canceled in your records |
| canceled already | Same canceled job, idempotent | Treat as success |
| processing, generation started | 409 job_generation_already_started, details.cancelable false | Keep polling; it will complete or fail |
| completed | 409 job_not_cancelable | Read the result instead |
Write a cancel that retries
Because the repeat is harmless, a client may retry a cancel after a network error without a guard. Read the response: a canceled job is success, a job_generation_already_started is a signal to stop trying and switch to polling for the real outcome. Never loop on a 409.
Keep the status vocabulary straight. The /v1/videos poll uses cancelled, with two l, and the jobs API uses canceled; the video docs list pending, in_progress, completed, failed and cancelled, while the jobs page lists queued, processing, completed, failed and canceled. A comparison written against one spelling will miss the other.
Billing on cancel
The docs say cancellation succeeds only before generation starts, and that failed jobs and failed queue admissions release or refund the reservation where applicable. They do not publish a separate rule for a canceled job beyond that, so check usage on the job after a cancel and compare it with your balance before you automate refunds in your own system.
A user-facing pattern
That pattern is accurate without promising a refund or a stop that the API does not offer.
- Show a cancel control only while the job is
queued. - On click, call cancel once and then read the status.
- If the answer is
job_generation_already_started, replace the button with "Rendering" and keep polling. - Store the final state, so a page reload does not offer cancel again.
Cancel and idempotency keys
A cancel is a different call from the submit, so it does not use the submit's idempotency key. If you cancel a job you created with a key and then submit the same payload again with the same key, the replay returns the original job, which is canceled. Use a new key when you deliberately want a fresh attempt at the same clip.
Keep that rule in your own code: a canceled job and its key are a pair, and a retry is a new key.
Limits
The docs do not say how long the window between queued and started is, so a cancel can lose a race with a worker. Your UI should expect a 409 and handle it quietly.
Sources
Related posts
More in Developers
- Caption a folder of videos in Python: thread pool, one key per file
Submit caption jobs from a Python thread pool without double billing: one Idempotency-Key per file, poll with next_poll_after_seconds, and the two 429s.
- Caption design colors: hex, rgb, rgba, transparent only, else 400
Sume's caption design.colors accepts hex, rgb(), rgba() or transparent and rejects other CSS with a 400. Fields, examples and a safe validator.
- Check an episode against TikTok upload specs before you post
TikTok's media guide lists MP4/H.264, 360 to 4096 px a side, 23 to 60 FPS and a 4GB cap. Use Sume video inspect to read your file's probe before uploading.
- Did the AI edit touch pixels outside the mask? Check it in numpy
BFL promises edits that leave the rest unchanged. Verify that claim on any model's output with a numpy diff outside your edit box, in about 15 lines.
Written by Sume