Cancel a completed job: 409 job_not_cancelable vs already started
Sume returns 409 job_not_cancelable when you cancel a finished job, and job_generation_already_started mid-run. How to tell them apart and handle each.

If POST /v1/jobs/:id/cancel returns 409 job_not_cancelable, the job is already terminal (for example completed), so there is nothing left to cancel; read the result instead. A different 409, job_generation_already_started, means the job is still running but generation work has begun, so it will finish or fail on its own and keeps billing.
Both are the conflict class: per MDN, 409 means the request conflicts with the current state of the target resource. Sume's cancel route only succeeds before generation starts, as the jobs and results page states.
What does job_not_cancelable look like?
The error envelope carries the usual request_id, plus details that say which state the job was in and when cancellation would have been possible. In the API's own test fixture for a completed job, the body has code: "job_not_cancelable", the message "API job can only be canceled before generation starts.", retryable: false, next_action: "inspect_events", and details: { status: "completed", cancelable_before: "generation_started" }.
Because retryable is false, a retry loop that treats every 409 as transient will spin on it forever. Treat it as an answer, not a failure.
How is it different from job_generation_already_started?
The errors page lists three job-state 409 codes: job_not_completed, job_not_cancelable, and job_generation_already_started. Only the last one means the job is alive.
In the docs, cancel after generation starts returns 409 job_generation_already_started with details.cancelable: false, and the job completes or fails normally. Read the status field of the job before deciding what to do next.
| Job state when you cancel | Response | What to do |
|---|---|---|
| queued, or processing before generation starts | Job becomes canceled | Stop polling; reservation is released |
| processing, generation started | 409 job_generation_already_started | Keep polling to terminal |
| completed, failed | 409 job_not_cancelable | Read the result or the error |
| already canceled | Same canceled job returned (idempotent) | Nothing, it is a no-op |
What does cancelling an already canceled job return?
The docs call cancel on a canceled job idempotent: it returns the same canceled job. The OpenAPI schema for the cancel response includes an idempotency_hit boolean described as true when the job was already canceled before this request, which lets a retrying client tell a fresh cancel from a replay.
This matters for shutdown hooks that cancel everything they submitted: a second pass over the same ids is safe.
A cancel helper that handles all three
The sketch below treats job_not_cancelable as a normal outcome and routes the caller to the right next read.
Note that cancelling is a request for a refund of work that has not started. Once job_generation_already_started is returned, budget for the job to bill and fetch its result.
import os, requests
BASE = "https://api.sume.com"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
def cancel(job_id: str) -> str:
r = requests.post(f"{BASE}/v1/jobs/{job_id}/cancel", headers=H, timeout=30)
if r.ok:
return "canceled"
err = r.json().get("error", {})
code = err.get("code")
if code == "job_not_cancelable":
return "terminal:" + err["details"]["status"]
if code == "job_generation_already_started":
return "running"
r.raise_for_status()
return "unknown"
print(cancel("job_123"))What does Sume not do here?
Sume does not let you stop a job after generation has started, and the docs are explicit about it. If you need a ceiling on spend, reduce the request before you submit it or cap the work on the run surfaces that support a spend cap. Keep the request_id from any 409 for support.
Quick checklist
The points above reduce to a short list you can paste into a runbook.
- Treat job_not_cancelable as final: retryable is false, so do not loop on it.
- Cancel only jobs you expect to be queued; use cancel_url from the envelope to decide.
- Keep the request_id from any 409 for support, and never log your API key.
- After job_generation_already_started, keep polling to a terminal state and budget for the bill.
- On shutdown, a second cancel pass over the same ids is safe because an already canceled job returns the same canceled job.
Sources
Related posts
More in Developers
- Cancel queued Sume jobs on SIGTERM during a deploy
On shutdown, cancel jobs you no longer need before they start and leave started ones alone. A 21-line Node handler using POST /v1/jobs/{id}/cancel.
- cancel_url is null on a processing job: when Sume allows cancel
Why a Sume job in processing can show cancelable false and cancel_url null: cancel depends on whether generation work started, not the status label.
- Check a video request against /v1/videos/models before you submit
Duration, resolution, size and seed errors cost a round trip. A short Python validator reads the model catalog and refuses a bad request locally first.
- Check a transparent GPT Image 2.5 PNG for real alpha in Python
A transparent GPT Image 2.5 result can still look opaque. Ask for background transparent as PNG, then check the alpha channel in Python: a 20-line script.
Written by Sume