Try-on double click: one Idempotency-Key per shopper and garment
Stop a double-clicked Try on button from running a Sume Format twice. Derive the Idempotency-Key from shopper, garment and version, and read the 200 replay.

Derive the Idempotency-Key from the thing being made, here the shopper id, the garment id and a version number you bump on purpose, and send it on every run create. A double click then returns 200 with the original receipt and idempotency_hit: true instead of starting a second run and charging for it. A random key per click makes the header useless.
This matters more now that ChatGPT has put a Try on button on product listings. Shoppers expect to tap it on a store's own page too, and taps get repeated on slow mobile connections. A try-on Format run produces images and a clip, so a duplicate is a real charge, not a rounding error.
What Sume does with the key
The Formats call doc spells out the replay table. The same key and the same body returns 200 with the original receipt, no second run and no second charge. The same key with a different body, including a different instruction or attachment list, is 409 idempotency_conflict and nothing runs. Two requests at the same moment with the same key give one winner and a 409 idempotency_key_in_use for the other, which is retryable: wait about a second and resend. A failed create, such as a 402 or 503, releases the key, so you can fix the cause and retry with the same key (read 2026-10-03, Sume Formats call docs).
Keys are scoped to one Format and can be up to 255 characters. The same key sent to two Formats starts two runs.
| Situation | Result | Your handler should |
|---|---|---|
| Same key, same body | 200, idempotency_hit: true | Show the existing run |
| Same key, different body | 409 idempotency_conflict | Fix the key derivation; do not retry |
| Same key, simultaneous | 409 idempotency_key_in_use | Wait about a second, resend |
| Same key after a failed create | Key released | Fix the cause, retry with the same key |
Deriving the key
A good key is built only from values that identify the job. If the shopper changes the reference photo, that is a different job and should get a new version suffix. If your instruction text contains a timestamp, the body changes on every click and you will hit 409 idempotency_conflict, which is the "unstable key derivation" case the docs warn about.
import os, requests
def start_tryon(shopper_id: str, sku: str, version: int, photo_url: str, garment_url: str):
key = f"tryon-{shopper_id}-{sku}-v{version}"
r = requests.post(
"https://api.sume.com/v1/formats/sume/sume-virtual-try-on/runs",
headers={
"Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
"Idempotency-Key": key,
},
json={
"instruction": "Vertical 9:16 try-on clip, no captions.",
"attachments": [
{"type": "input_image", "image_url": photo_url},
{"type": "input_image", "image_url": garment_url},
],
},
timeout=60,
)
r.raise_for_status()
body = r.json()
return body["data"]["id"], body.get("idempotency_hit", False)Related guards worth adding
Keys stop duplicates of the same request, not different requests from the same shopper. Add on_active_run: "reject" if you want a second, different try-on to fail with 409 format_run_in_progress while one is in flight, and set generation_spend_cap_usd per run so a bad loop cannot spend up to the Format's default. Both fields are in the Formats call doc. For continuing a finished run with a changed instruction rather than starting over, the previous_run_id errors post lists what can go wrong.
For catalog-scale jobs, where one key per SKU is the natural shape, see the bulk try-on post.
What the client should show on a replay
A replay is not an error, so do not show one. When idempotency_hit is true, the body is the original receipt: render whatever state that run is in, whether queued, running or finished. The shopper who tapped twice should see one progress indicator, not two and not a warning. Store the returned run id against your key so that a page reload can fetch the same run instead of posting again.
The two 409 codes need different treatment. idempotency_conflict is a bug in your code, because the same key was built from two different bodies, so log it loudly and fix the derivation. idempotency_key_in_use is normal under a fast double tap, so sleep about a second and resend the same request unchanged. Do not change the body on that retry, or you turn a harmless race into a real conflict.
One last trap: do not put volatile text in the instruction. A line such as "requested at 14:03" changes the body on every click and defeats the whole scheme. If you need a timestamp, keep it in your own database.
Sources
Related posts
More in Developers
- Typed try-on results: output_schema with a video URL and your SKU
Bind an output_schema to a Sume try-on run to get the video as a typed field, and keep your SKU outside it because identifiers do not round-trip.
- TTS from an accepted script: transcript_source instead of pasted text
Sume TTS can read an accepted script by script_revision_id and sentence_ids, not pasted text. MCP tools tts_source_get and tts_source_verify_spine support it.
- TTS volume 0.5 to 2: set narration gain before mixing with music
Sume TTS 1.0 generation_config.volume runs from 0.5 to 2 alongside speed 0.6 to 1.5. How to set narration level before you mix with a music bed.
- TTS mp3 bit_rate vs wav: fit a voiceover under the 10 MB Fabric limit
Sume TTS mp3 bit rates run 32k to 192k. At 128k a 300-second voiceover is about 4.8 MB, under the 10 MB Fabric audio limit. Mono 16 kHz wav is 9.6 MB.
Written by Sume