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.

5 min readSume
All posts

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`MeaningWhat your code should do
canceledThis call stopped a run in progressMark the order canceled. Show usage for what was already spent
no_opThe run had already finishedRead 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

  • cancelable on the receipt is true only while the run is queued or processing. 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_limited with error.details.scope set to write.
  • 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 4xx at create, an idempotent 200 replay and a skipped run 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

All Developers posts

Written by Sume