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.

5 min readSume
All posts

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.

What the proxy forwards (Sume docs, read 2026-10-05)
RouteBrowser sendsServer addsBrowser gets
POST /renderprompt onlymodel, duration, resolution, aspect_ratio, key, idempotency keyid, status, error code
GET /status/:idid in the pathkeystatus, file url, error
anything elsen/an/a404

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

All Developers posts

Written by Sume