Idempotency-Key from an order id and version, never a fresh uuid

A fresh uuid per request makes Idempotency-Key do nothing. Derive it from the order id plus a version you bump only to re-run. Scope: one Format, 255 chars.

5 min readSume
All posts

Build the Idempotency-Key from the thing the run makes: your order id, plus a version number that you raise only when you want a re-run. Do not derive it from the time of the request. The docs state that if you use a new uuidgen per request, the header has no effect, because a retry arrives with a different key and starts a second run, which Sume bills.

What a replay does

Send the header on every create. The result depends on what you send twice.

ReplayResult
Same key, same body200 with the original receipt and idempotency_hit: true. No second run, no second charge
Same key, different body409 idempotency_conflict. Nothing runs
Same key at the same momentOne request wins. The other gets 409 idempotency_key_in_use, which is retryable
Same key after a failed create (402, 503)Sume released the key. Fix the cause and retry with the same key

A different instruction or a different attachment list counts as a different body. That is the usual cause of an unexpected idempotency_conflict: the key is stable, but something in the body is not, such as a timestamp embedded in the instruction text.

Scope and size

The scope of a key is one Format. If you send the same key to two Formats, you start two runs, so the same order id sent to two Formats gives two runs. A key is up to 255 characters. The helper below hashes anything longer, so an unusually long id does not turn into a rejected request. It prints the stable key, a bumped key for a deliberate re-run, and the length of a hashed key.

import { createHash } from "node:crypto";

// Derive the key from the item the run makes. Never from the clock or a fresh uuid.
export function runKey(orderId, version = 1) {
  const key = `order-${orderId}-v${version}`;
  if (key.length <= 255) return key;
  return `order-${createHash("sha256").update(key).digest("hex")}`; // keys are up to 255 chars
}

console.log(runKey("8823"));                  // same order, same key: a replay returns idempotency_hit
console.log(runKey("8823", 2));               // bump the version only to force a re-run
console.log(runKey("x".repeat(300)).length);  // 70

Choosing the version

  • Keep the version at 1 for retries after a crash, a deploy or a client timeout. The replay returns the original receipt and idempotency_hit: true.
  • Raise it when the business wants a new result: the customer edited the brief, or the first output was rejected. That starts a new run with a new bill.
  • Store the key with the run id in your own table the moment the create returns. A 200 replay then lets you confirm that the id you hold is the one Sume returned.
  • Do not put secrets or personal data in the key. It is stored on the receipt as trigger.idempotency_key.
  • Do not reuse one key for a changed input. Change the version instead, so the new request has its own key.

Why the client matters too

The SDK's createSumeClient retries 408, 429, 5xx and transport failures, but it retries a POST only when the request carries an Idempotency-Key. Without one, a replay starts and bills a second run. subscribeFormatRun generates a key for you unless you pass your own, which is convenient for scripts. For anything that can be retried by a queue or a cron job, pass a key derived from your own record. See Calling a Format for the replay rules, and the nearby post on `idempotency_conflict` and `idempotency_key_in_use` for the two 409 codes.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume