Same idempotency key, new body: Sume Format run 409 vs 200 replay

Same Idempotency-Key and body on a Sume Format run gives 200 with idempotency_hit true. A changed body or attachments gives 409. Bulk replays stay 202.

5 min readSume
All posts

Send the same Idempotency-Key with the same body to POST /v1/formats/{handle}/{slug}/runs and Sume returns 200 with the original receipt and idempotency_hit: true, with no second run and no second charge. Send the same key with a different body and you get 409 idempotency_conflict, and nothing runs.

The behavior is in the Calling a Format docs. A different instruction or a different attachment list counts as a different body, which is the part people forget.

Replay outcomes

The scope of a key is one Format, and a key can be up to 255 characters. The same key sent to two Formats starts two runs.

Format run idempotency outcomes, from docs.sume.com/formats/call and /formats/bulk-runs, read 2026-10-09
SituationResult
Same key, same body200, original receipt, idempotency_hit: true
Same key, different body or attachments409 idempotency_conflict, nothing runs
Same key, two requests at the same momentOne wins; the other gets 409 idempotency_key_in_use, retryable after about one second
Same key after a failed create (402, 503, ...)Key was released; fix the cause and retry with the same key
Bulk queue, same key, same payload202 with the old queue (no idempotency_hit field)
Bulk queue, same key, different payload409 idempotency_conflict, details.queue_id names the original

Deriving the key

Derive the key from the item the run makes: your order id plus a version number that you bump only when you want a re-run. Do not use the time of the request, and do not call a new uuidgen per attempt, because then the header does nothing and a retry after a timeout starts a second paid run.

Example: order 8841 with version 1 gives the key order-8841-v1. A network timeout, then a retry with the same key and body, returns the same run. A deliberate re-run with new copy uses order-8841-v2.

import uuid

def run_key(order_id: int, version: int) -> str:
    return f"order-{order_id}-v{version}"

print(run_key(8841, 1))   # retry-safe
print(run_key(8841, 2))   # deliberate re-run
print(str(uuid.uuid4())[:8], "<- a fresh uuid per attempt defeats the header")

Handling the 409s

A conflict means your code reused a key for new work, so look for a version that did not change. idempotency_key_in_use is the other 409 and it is retryable: wait about one second, then send again to get the original run.

  • Bulk queues take the key as the header or a body idempotency_key; the header wins if both are sent.
  • Mint a fresh key per batch, because a spent key returns the old queue.
  • Store data.id from the first response and use the receipt URLs it carries.

Why the same-moment case matters

Two workers that retry on a timeout at the same instant can both send the key. Only one wins. The other gets idempotency_key_in_use, and the docs call it retryable: wait roughly a second, send again, and you receive the original run. Build the retry as part of the client and you never need a lock of your own. If a create fails with a 402 or 503, the key is released, so the same key is safe to reuse once the cause is fixed, for example after topping up credits.

Sources

Related posts

More in Formats

All Formats posts

Written by Sume