Proxy for a Sume video button: forward the click's Idempotency-Key
The docs proxy sample adds crypto.randomUUID() on every forwarded call, so a browser retry makes a second paid job. Take the key from the client for /v1/videos.

If your backend proxies a browser's video request to Sume, take the Idempotency-Key from the browser's request and forward it. Do not generate a new crypto.randomUUID() for each forwarded call. Sume returns the original job only when the key repeats, so a key created inside the proxy cannot protect against a browser that retries.
What the docs sample does
The authentication docs show a backend route that attaches the API key and an Idempotency-Key: crypto.randomUUID() to a forwarded avatar request. That is correct as a minimal example of keeping the API key off the client. For a button that users can double-click, or a mobile app that retries on a flaky network, the key has to exist before the first attempt, so the same value arrives on every retry.
| Key source | Retry from the browser | Result on Sume |
|---|---|---|
| new randomUUID in the proxy each call | new key | a second job, a second reservation |
| UUID made once per click, sent as a header | same key | original job returned |
| same key, edited prompt | same key, new body | 409 idempotency_conflict |
A proxy that forwards the key
The route below rejects a request with no key (400), validates the body fields it allows, and forwards the request to /v1/videos. It passes the status and body through, including the 202 and any 409. Put your own authentication and per-user limits in front of it.
export async function POST(request: Request) {
const key = request.headers.get("idempotency-key");
if (!key || key.length < 16) {
return Response.json({ error: "idempotency-key required" }, { status: 400 });
}
const { prompt, duration } = await request.json();
const upstream = await fetch("https://api.sume.com/v1/videos", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify({ model: "sume/auto", prompt, duration }),
});
return new Response(await upstream.text(), {
status: upstream.status,
headers: { "Content-Type": "application/json" },
});
}What goes wrong without it
A user double-clicks Generate. The browser sends two POSTs to your proxy. With a random key per forwarded call, Sume sees two different keys, accepts two jobs and reserves funds for both. With a client key the second request is a replay and returns the first job, so the user pays once and sees one clip.
In the browser
Create the UUID when the user presses the button, keep it in component state, and send it with the first fetch and with every retry of that click. A new click is a new key. If the user changes the prompt after the first send, create a new key too, because the same key with a new body is a 409 idempotency_conflict.
The docs say retries of an unsafe submit are only safe with a key, and that a 429 should be retried with the retry-after delay. Both rules apply here. See the 409 conflict and the 429 budgets.
- Never expose the Sume key to the browser; the proxy attaches it.
- Validate and clamp user input before forwarding, as the docs advise.
- Rate-limit the proxy per user, so a loop cannot drain the balance.
Sources
Related posts
More in Developers
- Budget 250 product shots: read each Sume model's price in code
A short Python script reads the pricing line from Sume's per-model endpoints route and prints the cost of 250 product shots. Reference rules and a worked table.
- Build IMAGE_REF tags in Python for up to 10 Omni references
A Python helper turns a list of up to 10 image URLs into an Omni Flash prompt with matching IMAGE_REF tags, 0-based, in list order. Runs as written.
- Bulk run item completed is not success: check before you publish
A Sume Format bulk child can be completed without being a success. Read each result and use per-child webhooks; no queue webhook exists.
- Bun 1.4.2 fixes an AsyncLocalStorage leak: per-request Sume keys
Bun 1.4.2 fixes an AsyncLocalStorage leak from 1.4.1. Carry a per-request Sume Idempotency-Key in the store, and check your Bun version first.
Written by Sume