Fresh Idempotency-Key per proxy call: why a Sume retry bills twice

If your server proxy mints a new Idempotency-Key on every request, a browser retry becomes a second paid Sume job. Forward the client's key instead; TypeScript.

5 min readSume
All posts

A proxy that sets Idempotency-Key: crypto.randomUUID() on every outgoing Sume request protects nothing when the caller retries. Each retry arrives as a new request, gets a new key, and Sume treats it as a new intent, so a second paid job can start. The key has to be created once per user action and passed through unchanged.

Sume's Authentication page shows a server-side proxy for browser and mobile clients, and its sample generates a fresh UUID per request. That is a fine default for a call that is never repeated. It stops being fine the moment a browser, a mobile client, or your own queue repeats the call after a timeout. This page covers the change that makes retries safe.

What Sume does with a repeated key

The docs describe the contract in three short rules. The details are in the table below, read 2026-10-09 from the pages cited at the end.

Idempotency rules on Sume submit endpoints, as of 2026-10-09 (Jobs and results, Generation admission, Errors and rate limits).
SituationWhat Sume doesWhat you do
Same key, same operation, same payloadReturns the original job instead of billing a second one; the envelope carries idempotency_hitSafe to retry after a timeout or network failure
Same key, different operation or payload409 idempotency_conflictUse a key again only for an exact retry
New key for the same intentTreated as a new requestNothing protects you; this is the proxy bug
Local worker timed outThe job may still be running and billingPoll the stored job id; do not submit the paid request again

The fix: the client owns the key

Have the browser or caller create one key per user action, for example when the person presses Generate, and send it as an Idempotency-Key header to your route. The proxy reads that header, refuses requests without one, and forwards it. A retry of the same action then reuses the same key, and Sume returns the original job.

The Authentication page also says to validate input and enforce your own authorization before forwarding. Keep both. Keep SUME_API_KEY on the server, as the page says, and never in frontend code.

export async function POST(request: Request) {
  const key = request.headers.get("idempotency-key");
  if (!key) {
    return new Response("Idempotency-Key header required", { status: 400 });
  }
  const response = await fetch("https://api.sume.com/v1/image-1.0/generate", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUME_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": key,
    },
    body: await request.text(),
  });
  return new Response(await response.text(), {
    status: response.status,
    headers: { "Content-Type": "application/json" },
  });
}

Choosing the key

Any string that identifies the intent works. Docs examples use readable keys such as hero-shot-2026-08-03-001 and avatar-batch-001-item-001. A business id (an order line, a shot number) is better than a random value when your own job queue can also retry, because the queue worker can rebuild the same key without storing it.

If the retry changes the prompt, change the key too. Reusing the key with a different payload returns 409 idempotency_conflict, which is the signal that you are sending two intents under one key. When a request is refused with 429 queue_full, the docs say to retry with the same key after capacity opens, so keep the key around until the job id is stored.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume