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.

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.
| Situation | What Sume does | What you do |
|---|---|---|
| Same key, same operation, same payload | Returns the original job instead of billing a second one; the envelope carries idempotency_hit | Safe to retry after a timeout or network failure |
| Same key, different operation or payload | 409 idempotency_conflict | Use a key again only for an exact retry |
| New key for the same intent | Treated as a new request | Nothing protects you; this is the proxy bug |
| Local worker timed out | The job may still be running and billing | Poll 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
- Python asyncio loop for a Sume job: next_poll_after_seconds
A runnable httpx and asyncio loop for GET /v1/jobs/{id}/status that honors next_poll_after_seconds, backs off otherwise, and leaves the job running on timeout.
- Python cost cap for a mixed media job: round up, then refuse
A 25-line Python estimator with Decimal prices that rounds the total up to the cent, as Sume's reservation does, and exits before submit if it is over your cap.
- Python httpx 429 handler for Sume: retry-after, then ratelimit-reset
A small async Python wrapper for the Sume API that waits on retry-after, falls back to ratelimit-reset, and never retries a POST that lacks an Idempotency-Key.
- Python verifier for Sume webhooks: rotation header and empty secrets
A Python function that checks the sume-v1 HMAC over timestamp.raw_body, accepts either signature during rotation, and refuses an empty secret. Under 30 lines.
Written by Sume