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.

6 min readSume
All posts

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.

Replay results, read 2026-10-03
SituationResultYour handler should
Same key, same body200, idempotency_hit: trueShow the existing run
Same key, different body409 idempotency_conflictFix the key derivation; do not retry
Same key, simultaneous409 idempotency_key_in_useWait about a second, resend
Same key after a failed createKey releasedFix 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

All Developers posts

Written by Sume