Stream Sume job progress to a browser with SSE and Node polling

Sume has no SSE or WebSocket job stream, so relay it: a small Node endpoint polls the status route and pushes text/event-stream messages to EventSource.

6 min readSume
All posts

Run a small server of your own that polls GET /v1/jobs/{id}/status and writes each change to the browser as a text/event-stream response, which the built-in EventSource consumes. The Sume Developer API itself offers polling, sync waits and webhooks, not SSE, so the relay belongs in your backend and keeps the API key off the client.

The wire format

MDN describes the stream format: UTF-8 text with the text/event-stream content type, fields such as event: and data:, and a blank line ending each message. Lines that start with a colon are comments, handy as keep-alives. The browser reconnects automatically when the connection drops.

SSE fields used by the relay (read 2026-10-04)
FieldPurposeSent when
event: statusNames the event for addEventListenerStatus changed
data: {...}JSON of state and flagsStatus changed
: keep-aliveComment, ignored by the clientEvery poll with no change

Relay

This Node 18 relay stops when data.terminal is true, and it waits next_poll_after_seconds between polls with a 2 second floor.

import http from "node:http";

const API = "https://api.sume.com/v1";
const key = process.env.SUME_API_KEY;
if (!key) throw new Error("SUME_API_KEY is not set");
const headers = { "x-api-key": key };
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

http.createServer(async (req, res) => {
  const id = new URL(req.url!, "http://x").searchParams.get("job");
  if (!id || !/^job_[A-Za-z0-9_]+$/.test(id)) { res.writeHead(400).end(); return; }
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
  let closed = false; req.on("close", () => (closed = true));
  let last = "";
  while (!closed) {
    const r = await fetch(`${API}/jobs/${id}/status`, { headers });
    if (!r.ok) { res.write(`event: error\ndata: ${JSON.stringify({ status: r.status })}\n\n`); break; }
    const { data } = await r.json();
    if (data.sume_status !== last) {
      last = data.sume_status;
      res.write(`event: status\ndata: ${JSON.stringify({ status: data.sume_status, terminal: data.terminal })}\n\n`);
    } else res.write(": keep-alive\n\n");
    if (data.terminal) break;
    await sleep(Math.max(2, data.next_poll_after_seconds ?? 2) * 1000);
  }
  res.end();
}).listen(8787);

Browser side

In the browser: const es = new EventSource('/stream?job=' + id); es.addEventListener('status', (e) => render(JSON.parse(e.data))); and call es.close() on a terminal status, otherwise the browser reconnects and you poll again.

Limits to plan for

Each open stream polls Sume once per interval, so many viewers of one job should share one poller. The job id check above keeps callers from steering the upstream path. Authenticate your own endpoint, because the relay reads with your key.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume