Browser voice app that starts Sume jobs: keep the key on your server
Voice apps run in the browser over WebRTC, but Sume keys belong on a server. A route handler that holds the key, allowlists models, reuses idempotency keys.

If a voice app runs in the browser and can start Sume generation jobs, the browser must not hold the Sume API key. Sume's authentication docs say browser and mobile clients should call your backend, and the backend attaches the key.
OpenAI's Realtime guide puts browser sessions on WebRTC, so the page already talks to two parties. Only one of them, your server, should know the Sume key.
Who holds what
From the Sume authentication docs and the OpenAI Realtime guide, read 2026-10-03.
| Piece | Lives in | Rule |
|---|---|---|
| Voice session in the browser | Browser | WebRTC, per the Realtime guide |
| Voice session on a server | Your server | WebSocket, per the Realtime guide |
| Sume API key | Server environment variable | Never in frontend JavaScript or mobile apps |
| Job submit | Your route handler | Attaches the key; validates input first |
| Job result | Your route handler or a webhook | Poll status or accept a signed webhook |
A route handler that holds the key
The handler below follows the proxy shape in Sume's docs, with three additions. It allowlists the models the page may ask for, it forwards only model and prompt, and it takes the idempotency key from the client instead of minting a new one per request. A fresh random key on every call defeats idempotency, because a retry of the same click would create a second paid job.
const ALLOWED = new Set(["sume/auto"]);
export async function POST(request: Request) {
const key = request.headers.get("idempotency-key");
if (!key) return new Response("idempotency-key required", { status: 400 });
const { model, prompt } = await request.json();
if (!ALLOWED.has(model) || typeof prompt !== "string" || prompt.length > 2000) {
return new Response("bad request", { status: 400 });
}
// enforce your own user authorization here before forwarding
const upstream = await fetch("https://api.sume.com/v1/images", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SUME_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify({ model, prompt, mode: "async" }),
});
return new Response(await upstream.text(), {
status: upstream.status,
headers: { "Content-Type": "application/json" },
});
}The browser side of the contract
Small rules that keep the split honest.
- The page generates one idempotency key per user intent and reuses it for retries of that intent.
- The page polls your status route, not Sume. Your route reads
GET /v1/jobs/:id/statuswith the key attached. - There is no push channel from Sume: the Developer API has no SSE or WebSocket transport, so the page polls or your server relays a webhook.
- Rotate the key if it ever appears in logs or chat history, as the authentication docs advise.
Webhook alternative
If you would rather not poll from the page, submit with mode: "webhook" and a public HTTPS webhook_url on your server. Sume sends terminal events only, signed with HMAC SHA 256, and your receiver should refuse to run with an empty secret. Keep polling available for deliveries that never arrive, as the webhook docs recommend.
Sources
Related posts
More in Developers
- C2PA 2.2: file types that can carry credentials vs Sume outputs
C2PA 2.2 manifests can be embedded in JPEG, PNG, WebP, SVG, MP4, MOV and more. How that list lines up with the formats Sume image and video jobs return.
- C2PA 2.2 in plain terms: manifests, hard and soft bindings
C2PA 2.2 describes signed manifests with hash-based hard bindings and fingerprint or watermark soft bindings. What that implies after a re-encode or trim.
- Watch the Sume video catalog for new ids and changed limits in Python
Fetch GET /v1/videos/models, save a snapshot and diff new ids, removed ids and changed durations or resolutions. A Python script, testable offline.
- Claude Code 2.1.285 lists WebSocket MCP servers; Sume uses HTTP
Claude Code 2.1.285 shows WebSocket MCP servers in claude mcp list. Sume's hosted MCP is a remote HTTP server, added with --transport http.
Written by Sume