A tiny Node proxy so a browser can order a 4K Omni clip safely

Sume keys are server-side only. A short Node proxy exposes POST and GET routes for one 4K Gemini Omni Flash 1.1 clip, with an Idempotency-Key and pinned fields.

4 min readSume
All posts

Never call Sume from the browser. Put a small server between them that holds SUME_API_KEY, accepts only the fields you allow, fixes the model, resolution and duration so a user cannot raise your bill, and forwards the job id back. The Node proxy below does that in about 30 lines, for one 4K Gemini Omni Flash 1.1 clip.

Why the key stays on the server

Sume's SDK docs say it directly: a Sume API key spends your credits, there is no browser-safe variant, and you must never put a key in client JavaScript, a mobile bundle or a NEXT_PUBLIC variable. Put your own endpoint in front and make the Sume request from there.

A proxy gives you three more things. It lets you pin the expensive parameters, so a user cannot ask for a 30-second 1080p job on a 4K clip button. It lets you attach an Idempotency-Key that you derive from your own order id. And it gives you a place to apply your own per-user limits before Sume's limits matter.

What the proxy allows

Gemini Omni Flash 1.1 is in the Sume catalog at 3 to 10 seconds, 360p to 4K, 16:9 or 9:16, with native synced audio. The proxy pins model, resolution and duration, and lets the user choose only the prompt and the aspect ratio. Treat every other field as untrusted.

Fields the proxy accepts and pins (Sume docs, read 2026-10-05)
FieldWho sets itValue
modelProxygemini-omni-flash-1.1
resolutionProxy4K
durationProxy8
promptUserLength-limited text
aspect_ratioUser, checked16:9 or 9:16
Idempotency-KeyProxyFrom your own order id

The proxy

Save as proxy.mjs, set SUME_API_KEY, and run node proxy.mjs on Node 18 or newer. POST /clip with a JSON body returns the Sume job id. GET /clip/JOB_ID returns the job status. There is no authentication in the sample, so put your own session check where the comment says to.

import http from "node:http";
const BASE = "https://api.sume.com";
const auth = { Authorization: `Bearer ${process.env.SUME_API_KEY}`, "Content-Type": "application/json" };
const send = (res, code, obj) => { res.writeHead(code, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };

http.createServer(async (req, res) => {
  // TODO: check your own user session here before anything else
  if (req.method === "POST" && req.url === "/clip") {
    let raw = "";
    for await (const chunk of req) raw += chunk;
    const { prompt, aspect_ratio = "16:9", order_id } = JSON.parse(raw || "{}");
    if (!prompt || prompt.length > 500 || !order_id) return send(res, 400, { error: "bad input" });
    if (!["16:9", "9:16"].includes(aspect_ratio)) return send(res, 400, { error: "bad aspect" });
    const r = await fetch(`${BASE}/v1/videos`, {
      method: "POST",
      headers: { ...auth, "Idempotency-Key": `order-${order_id}` },
      body: JSON.stringify({ model: "gemini-omni-flash-1.1", prompt, aspect_ratio, resolution: "4K", duration: 8 }),
    });
    return send(res, r.status, await r.json());
  }
  const m = req.url.match(/^\/clip\/(job_[\w-]+)$/);
  if (req.method === "GET" && m) {
    const r = await fetch(`${BASE}/v1/videos/${m[1]}`, { headers: auth });
    const j = await r.json();
    return send(res, r.status, { id: j.id, status: j.status, error: j.error });
  }
  send(res, 404, { error: "not found" });
}).listen(8787);

Choices in the code

The poll route returns only the id, status and error. It does not forward unsigned_urls, because those URLs need your key. Add a third route that streams the file after you check that the job belongs to the signed-in user. Store the mapping from your order id to the job id in your database, since a Sume job is only readable inside the workspace that created it.

The Idempotency-Key comes from the order id, so a double click or a retried request returns the original job and does not bill a second 4K render. If the user changes the prompt for the same order, Sume answers 409, which is your cue to issue a new order id.

Hardening list

  • Add a per-user daily cap before the Sume call. Your cap is the real budget control.
  • Return Sume's request_id in your logs, not to the browser.
  • Handle 402 insufficient_credits as an internal alert, and show users a neutral message.
  • Use a webhook with callback_url if you do not want the browser to poll through you.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume