Retry a failed Format create with the same Idempotency-Key

After a 402 or 503 on a Format create, Sume releases the Idempotency-Key. Fix the cause, then retry with the same key instead of minting a new one.

5 min readSume
All posts

Yes: reuse it. When a Format create fails with 402 or 503, Sume releases the Idempotency-Key, so the same key works again once you have fixed the cause. Keep the key stable across all your attempts for one order. If you generate a new key per attempt, an attempt that did land (for example, one whose response you never saw) can start a second run next to the first.

Which failures free the key

The create call can fail before a run exists. Nothing ran in these cases, and nothing was charged: a 4xx at create costs nothing. What to do depends on the code.

Status`error.code`Do this, with the same key
402insufficient_creditsFund the workspace (next_action: add_funds), then send again
402organization_wallet_not_provisionedAn admin must fund the organization wallet, then send again
503studio_agent_upstream_unavailableA Sume-side outage. Retry the same request later
409idempotency_key_in_useA duplicate is in flight. Wait about one second, then send again to get the original run
409idempotency_conflictDo not retry. The body differs from the first request that used this key

The two 409 codes behave differently. idempotency_key_in_use is marked retryable: true, and the retry returns the original run once the first request has finished. idempotency_conflict is a bug in your key derivation or your body, so repeating it only repeats the error.

A retry helper

The sample sends the create with a caller-supplied key, retries only the retryable cases after a one-second wait, and stops on a 402 with a message that tells the operator what to do. It reads SUME_API_KEY and optionally SUME_API_BASE_URL from the environment. It accepts the receipt either wrapped in data or bare.

const key = process.env.SUME_API_KEY;
const base = process.env.SUME_API_BASE_URL ?? "https://api.sume.com/v1";
if (!key) throw new Error("set SUME_API_KEY");

// A create that failed (402, 503) released the Idempotency-Key. Reuse it after the fix.
export async function createWithKey(path, body, idemKey, attempts = 3) {
  for (let i = 1; i <= attempts; i++) {
    const res = await fetch(`${base}${path}`, {
      method: "POST",
      headers: { "x-api-key": key, "content-type": "application/json", "idempotency-key": idemKey },
      body: JSON.stringify(body),
    });
    if (res.status === 409 || res.status === 503) {
      const err = (await res.json()).error;
      // idempotency_key_in_use is retryable: wait about one second, send the same request
      if (err?.code === "idempotency_key_in_use" || res.status === 503) {
        await new Promise((r) => setTimeout(r, 1000));
        continue;
      }
      throw new Error(err?.code);
    }
    if (res.status === 402) throw new Error("insufficient_credits: add funds, then call again with the same key");
    return res.json();
  }
  throw new Error("still failing after retries");
}

const run = await createWithKey("/formats/acme/product-promo/runs", { input: {} }, "order-8823-v1");
console.log(run.data?.id ?? run.id);

Notes

  • Do not retry 402 in a loop. Funding is a human or billing action, so surface it and let the order wait.
  • Cap your attempts. Three is plenty for a 503; after that, alert and keep the order in a pending state.
  • A 429 rate_limited on a create is also retryable with the same key. Wait for retry-after seconds; a create spends the write budget.
  • A 200 with idempotency_hit: true means an earlier attempt landed. Store that run id and do not create anything else.
  • The key's scope is one Format, so keep the key and the Format paired in your records.

Order of operations in a worker

Derive the key before the first attempt and store it on the order. On each attempt, send the same key and the same body. If any attempt returns a receipt, persist data.id and stop. If you run out of attempts, leave the order pending with the last error.code, and let a later job send the identical request again. Changing the body between attempts, even by adding a timestamp to the instruction, turns a harmless retry into 409 idempotency_conflict.

The case this protects

The dangerous failure is the ambiguous one: your client times out, or the connection drops after the request left. You cannot know whether the run exists. With a stable key, the retry either creates the run or returns the original with idempotency_hit: true. Without one, the retry may create a second run, and each run is billed. The SDK's createSumeClient follows the same rule: it retries a POST only when an Idempotency-Key is present. Read the replay table in Calling a Format, and the full error list in Errors and spend.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume