A 30-line Node proxy so a browser can start a Sume video, no key
A node:http server with only POST /render and GET /status/:id. It fixes the model and clip size and keeps SUME_API_KEY on the server, away from the browser.

A browser must never hold your Sume key, so put a small server between the page and the API that exposes exactly two routes: POST /render to start a clip and GET /status/:id to read it. The node:http proxy below does that in about thirty lines and fixes the model, length and resolution on the server.
If a demo page used to call the OpenAI Videos API directly with a pasted key, which OpenAI lists as removed on 2026-09-24, the port is an opportunity to close that hole instead of repeating it.
What the proxy decides, not the browser
The browser sends only a prompt. The server picks gemini-omni-flash-1.1, 5 seconds, 720p and 16:9, all inside that model's documented 3 to 10 second window. This limits what one visitor can spend per click: at the repo-documented rate of $0.125 a second, each render is 0.625, billed $0.63.
The status route accepts only ids that match a pattern, so a visitor cannot turn it into a general GET proxy against api.sume.com. It returns three fields, and nothing from the upstream body beyond them.
| Route | Browser sends | Server adds | Browser gets |
|---|---|---|---|
| POST /render | prompt only | model, duration, resolution, aspect_ratio, key, idempotency key | id, status, error code |
| GET /status/:id | id in the path | key | status, file url, error |
| anything else | n/a | n/a | 404 |
The server
Run it with Node 22 or later (it uses the global crypto and fetch), set SUME_API_KEY, and call POST http://localhost:8787/render with a JSON body. Save it as proxy.mjs. The body size cap and prompt length cap run before any outbound call, so oversized input costs nothing.
import { createServer } from "node:http";
const API = "https://api.sume.com/v1/videos";
const auth = { Authorization: `Bearer ${process.env.SUME_API_KEY}` };
const ID = /^\/status\/([A-Za-z0-9_-]{6,64})$/;
createServer(async (req, res) => {
const send = (code, obj) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(obj)); };
try {
if (req.method === "POST" && req.url === "/render") {
let raw = ""; for await (const c of req) { raw += c; if (raw.length > 4096) return send(413, {}); }
const { prompt } = JSON.parse(raw);
if (typeof prompt !== "string" || prompt.length > 600) return send(400, { error: "bad prompt" });
const r = await fetch(API, { method: "POST", signal: AbortSignal.timeout(30_000),
headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": req.headers["x-request-key"] ?? crypto.randomUUID() },
body: JSON.stringify({ model: "gemini-omni-flash-1.1", prompt, duration: 5, resolution: "720p", aspect_ratio: "16:9" }) });
const j = await r.json();
return send(r.status, { id: j.id, status: j.status, error: j.error?.code });
}
const m = req.method === "GET" && ID.exec(req.url);
if (!m) return send(404, {});
const j = await (await fetch(`${API}/${m[1]}`, { headers: auth, signal: AbortSignal.timeout(30_000) })).json();
send(200, { status: j.status, url: j.unsigned_urls?.[0], error: j.error });
} catch { send(502, { error: "upstream" }); }
}).listen(8787);
What it still needs before production
This is a skeleton. Add per-visitor rate limiting, because the biggest risk of a public render button is not the key, it is one person clicking it a thousand times. Sume answers 429 rate_limited when your key exceeds its budget, and that budget is shared by all your visitors.
Serve the browser page and the proxy from the same origin, or add the right CORS headers deliberately. The stored post on browser CORS errors walks through the failure you will otherwise see.
Return the file URL only if the page needs it. Many pages just show the clip, and the content route needs your credentials, so streaming the file through the proxy may suit you better.
- Send an x-request-key header from the page so a double click returns the same job.
- Never log the Authorization header.
- Cap daily spend with a counter on the server.
Polling from the page
The page should poll GET /status/:id every ten seconds or so and stop on completed, failed or cancelled, the three terminal words. Show the user a plain waiting message, because a five-second Omni clip is not instant. When the status is completed, the response carries a url field the page can use as a video source, subject to the access note above.
Sources
Related posts
More in Developers
- 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.
- 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.
Written by Sume