Cancel a Format run: cancel_effect canceled vs no_op, and the bill
Cancel is idempotent. cancel_effect says canceled or no_op, a canceled run never sends a webhook, and generation that finished is still billed.

Call POST /v1/format-runs/{run_id}/cancel with a key that has formats:write. The call is idempotent, and the response is the current receipt with a cancel_effect field. canceled means this call stopped a run that was in progress. no_op means the run had already finished before your call arrived. Either way you pay for the generation the run completed before the cancel, and usage on the receipt shows how much.
Read cancel_effect, not just the status code
Both outcomes return the receipt, so the HTTP status does not tell you which one happened. If you cancel because a user pressed a button, the difference matters. With no_op the run finished first and probably holds a usable result: the media is in artifacts[], and you should offer it rather than say the job was stopped.
| `cancel_effect` | Meaning | What your code should do |
|---|---|---|
canceled | This call stopped a run in progress | Mark the order canceled. Show usage for what was already spent |
no_op | The run had already finished | Read the receipt. Use the result if the run completed |
A canceled run sends no webhook
A canceled run never delivers a run webhook, and a skipped run does not either, because a skipped run is already terminal on the create response. If your integration is webhook-only, cancel is the one path where nothing will arrive. Treat the cancel response itself as the final state, and clear any timer or pending record you keep for that run id. The envelope status of OK or ERROR also exists only for completed and failed runs, not for cancel.
Sample
The function below posts the cancel and returns the effect and the usage block. It reads the key from SUME_API_KEY and accepts an alternate base URL for tests.
const key = process.env.SUME_API_KEY;
const base = process.env.SUME_API_BASE_URL ?? "https://api.sume.com/v1";
if (!key) throw new Error("set SUME_API_KEY");
export async function cancelRun(runId) {
const res = await fetch(`${base}/format-runs/${runId}/cancel`, {
method: "POST",
headers: { "x-api-key": key },
});
if (!res.ok) throw new Error(`cancel failed: ${res.status}`);
const run = await res.json();
const effect = run.data?.cancel_effect ?? run.cancel_effect;
// canceled: this call stopped a run in progress. no_op: it had already finished.
return { effect, usage: run.data?.usage ?? run.usage };
}
console.log(await cancelRun(process.argv[2] ?? "arun_demo"));Details that trip people
cancelableon the receipt istrueonly while the run isqueuedorprocessing. Check it before you show a cancel button.- The cancel counts as a write, so it spends the write budget, not the read budget. Many cancels in a loop can return
429 rate_limitedwitherror.details.scopeset towrite. - Cancel does not refund finished work. The docs say the same about failures: if a later step fails, the generation that already finished is not refunded.
- A client-side timeout does not cancel anything. If your HTTP client gives up, the run keeps executing and spending until you call cancel or it finishes.
- A
4xxat create, an idempotent200replay and askippedrun cost nothing, so there is nothing to cancel there.
Cancel or wait: a quick rule
Cancel when the result is no longer wanted and the run is still early, because you pay for generation that finished before the cancel. Do not cancel to "retry faster". A retry is a new run with its own cap and its own bill, and the first run keeps whatever it already generated. If you only lost your handle on the run, the cheaper move is to read it again by id, since the receipt holds status_url, result_url and cancel_url and the run keeps going either way.
When a run is part of a larger workflow, keep the cancel decision on your side of the order record. Write the intent first (cancel requested), call cancel, then store cancel_effect and usage as the outcome. If the call itself times out, repeat it: the endpoint is idempotent, so a second call returns the same kind of receipt and never double-cancels.
Check the spend afterwards
The receipt's usage.billable_amount_usd_micros counts reserved and captured generation spend and does not include the agent's LLM turn. For the real wallet effect use usage.debited_usd_micros, or GET /v1/usage?run_id=, and wait for final to be true before you reconcile. The Runs and results page describes the cancel call and the usage fields.
Sources
Related posts
More in Developers
- Cap Sume spend from an agent loop: dry_run, max_spend_usd and run caps
Four optional guards cap what an automated Sume caller can spend: dry_run, max_spend_usd, generation_spend_cap_usd on Formats, and a balance check.
- Caption design colors: hex, rgb(), rgba() or transparent only
Sume caption design colors take hex, rgb(), rgba() or transparent. Other CSS syntax is rejected at request time, so a bad color costs nothing.
- Captions out of sync with the audio: check STT word times and offsets
Captions running early or late usually trace to an unapplied offset. How Sume STT word times work, which offset to add, and a Python merge that applies it.
- Chapter timestamps for narrated audio from concat segment offsets
Join one TTS file per chapter with timeline audio concat, then turn the returned segments[] start offsets into mm:ss chapter lines with a short Python script.
Written by Sume