Shopper leaves mid try-on: cancel the Format run, mind the gap

Cancel a Sume Format run with POST /v1/format-runs/{id}/cancel when a shopper leaves a try-on. Canceled runs send no webhook, and finished work is billed.

6 min readSume
All posts

When a shopper leaves a try-on, cancel the run with POST /v1/format-runs/{run_id}/cancel. It needs formats:write, is idempotent, and returns the current receipt with a cancel_effect of canceled if it stopped a run in flight or no_op if it had already finished. Generation completed before the cancel is billed, and usage reports it. A canceled run never sends a webhook, so a webhook-only integration hears nothing.

This matters more after ChatGPT's new Try on button made one-tap try-ons a normal expectation (read 2026-10-03, OpenAI help page). Shoppers tap, get bored and leave. A try-on run that makes images and a clip keeps spending while nobody is watching.

What cancel does and does not do

A cancel stops a run in flight. It does not refund what the run already generated. Cancel early and the bill is small; cancel after the first frame is built and you pay for that frame. Set generation_spend_cap_usd on the create call as the other half of the guard, since a run can never spend past its own cap.

Because the call is idempotent, you can send it from more than one place, for example when the page unloads and again from a server job, without worrying about a second effect. Read cancel_effect to know which happened.

Cancel behaviour, read 2026-10-03
SituationResultWhat to do
Run in flightcancel_effect: canceledMark your row canceled
Run already finishedcancel_effect: no_opUse the finished receipt
Webhook armedNo delivery for a canceled runUse the receipt the cancel call returns
Spent before cancelBilled, shown in usageLog billable_amount_usd_micros
import os, requests

def cancel_tryon(run_id: str):
    r = requests.post(
        f"https://api.sume.com/v1/format-runs/{run_id}/cancel",
        headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
        timeout=30,
    )
    r.raise_for_status()
    data = r.json()["data"] if "data" in r.json() else r.json()
    return data.get("cancel_effect"), data.get("usage", {}).get("billable_amount_usd_micros")

The webhook gap

A webhook-driven integration marks a try-on finished when format.run.terminal arrives. Cancel is the one path where nothing arrives, so a row you mark as canceled yourself would otherwise sit in a running state forever. Write the canceled state from the cancel response, and let the webhook handle the completed and failed cases.

Also handle the race. A shopper can leave just as a run finishes, so the cancel returns no_op and the webhook arrives anyway. Make your handler idempotent on run_id, which is stable across retries and is the dedupe key, and treat the later of the two events as authoritative only if it moves the row forward.

Do not poll a canceled run waiting for a terminal webhook. Fetch the receipt once from GET /v1/format-runs/{run_id} if you want the final usage figure.

When to cancel and when to let it finish

A useful rule is to cancel when the output is no longer wanted and nothing else depends on it. If the result will still be useful later, for example the shopper might come back, let it finish and keep the URL, since you will have paid for most of it anyway. The Sume result URLs are durable media.sume.com links.

For retry after a failure without double charging, reuse the same Idempotency-Key as in the double-click post, and to extend a finished run use previous_run_id as described in the continuation errors post. For catalogue-scale work, see the bulk post.

Logging what a cancelled try-on cost

Record three numbers for every run: when it was created, whether it ended completed, failed or canceled, and the billable amount from the receipt. After a week you can answer a useful question: how much did abandoned try-ons cost, and would a lower spend cap or an earlier cancel have saved it. Sume gives usage.billable_amount_usd_micros in millionths of a dollar, so divide by one million before you show it.

If the abandoned share is high, change the UI before you change the code. Show progress, set an expectation of how long a try-on takes, and let the shopper continue browsing while it runs, then notify them. A shopper who knows a result is coming leaves less often.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume