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.

5 min readSume
All posts

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.

Where the Idempotency-Key is created and what a retry does, per the Sume docs (read 2026-10-08)
Key sourceRetry from the browserResult on Sume
new randomUUID in the proxy each callnew keya second job, a second reservation
UUID made once per click, sent as a headersame keyoriginal job returned
same key, edited promptsame key, new body409 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

All Developers posts

Written by Sume