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.

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.
| Field | Who sets it | Value |
|---|---|---|
| model | Proxy | gemini-omni-flash-1.1 |
| resolution | Proxy | 4K |
| duration | Proxy | 8 |
| prompt | User | Length-limited text |
| aspect_ratio | User, checked | 16:9 or 9:16 |
| Idempotency-Key | Proxy | From 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
- tqdm in Jupyter for a 30-second AI video render, no percentage
Sume gives job statuses, not a percent. Use a tqdm elapsed-time bar with the status as its label, and stop on terminal. Runs in a notebook cell.
- Transcribe 1,000 twenty-second clips: pack 20 per file, then run STT
1,000 clips of 20 s cost $10.00 on Sume STT if billed one minute each, but $4.00 if you pack 20 clips per file with Timeline audio.
- Transcribe a 9-minute recording in two calls for ten cents
Detach the audio at 16 kHz mono ($0.01), then send it to Sume STT ($0.01 a minute): nine minutes of speech to text for $0.10 in two requests.
- Transcribe a MAI-Voice-2.1 clip with Sume STT: Python audio_url run
Host a MAI-Voice-2.1 or Flash clip at a public HTTPS URL and Sume STT returns text, word times and sentences for 1 cent a minute. A 25-line Python run.
Written by Sume