Why streaming?
If your app needs a steady supply of fresh randomness — a tick every second for a live multiplayer game, a sample every 100 ms for a simulator, a draw per visitor for a real-time dashboard — calling /api/floats on a loop adds an HTTP round-trip per draw. The /api/stream endpoint keeps a single connection open and pushes outcomes as Server-Sent Events (SSE) on a timer you control. Every emitted frame is still a self-contained, individually verifiable outcome — nothing about the cryptography changes.
The shape of the stream
Unlike the rest of the API, the response is text/event-stream, not JSON. The server holds the connection open and writes a sequence of frames. Each one looks like this:
id: my-app:0:0
event: outcome
data: {"outcome":[0.4823],"clientSeed":"my-app","serverHash":"9f86d0...","nonce":0,"cursor":0,"count":1,"endpoint":"floats","created":1716566400000,"outcomeId":"my-app:0:0"}
The data: payload is the exact same JSON shape the matching non-streaming endpoint returns, plus an outcomeId field of clientSeed:cursor:nonce for easy correlation. You can replay each outcome through /api/verifyServerHash on its own to confirm it really came from the published server seed.
Between outcomes the server sends SSE comment frames (lines starting with :) every ~15 seconds as heartbeats so intermediate proxies don't time the connection out. When the connection ends — because it hit the 10 minute cap or you blew through your daily quota — the server writes one final frame and closes:
event: done
data: {"reason":"max_duration","count":600,"durationMs":600000}
Vanilla JS client
Browsers ship a built-in EventSource. No dependencies required:
const params = new URLSearchParams({
endpoint: "floats",
clientSeed: "live-game-42",
count: "1",
intervalMs: "1000",
});
const url = `https://api.provable.io/api/stream?${params}`;
const es = new EventSource(url);
es.addEventListener("outcome", (event) => {
const o = JSON.parse(event.data);
// o.outcome is the same shape as /api/floats would return.
console.log(o.outcomeId, o.outcome);
});
es.addEventListener("done", (event) => {
const { reason, count, durationMs } = JSON.parse(event.data);
console.log(`stream closed after ${count} outcomes (${reason}, ${durationMs}ms)`);
es.close();
});
es.onerror = () => {
// EventSource auto-reconnects on transient drops; close to stop billing.
es.close();
};
// Stop the stream (and stop being billed) whenever the user navigates away
// or the game ends.
window.addEventListener("beforeunload", () => es.close());
Switch to any of the other generators by changing endpoint and passing the matching parameters: ints (with min/max), dice (with notation), shuffle / pick (with items), bytes (with encoding), or gaussian (with distribution, mean, etc.).
Resuming after a disconnect
Every event: outcome frame ships with an id: line of clientSeed:cursor:nonce — the same value as the outcomeId in the payload. Browsers' built-in EventSource already remembers the last id it saw and replays it as a Last-Event-ID request header when it auto-reconnects after a transient network blip; the server reads that header and only emits outcomes whose (cursor, nonce) is strictly after it, so reconnects don't double-count or skip outcomes for the same clientSeed.
You don't have to do anything for vanilla EventSource — it's automatic. Reusing the same clientSeed is what makes it work:
const es = new EventSource(url);
es.addEventListener("outcome", (event) => {
const o = JSON.parse(event.data);
console.log(o.outcomeId); // e.g. "live-game-42:0:17"
});
// On a transient disconnect the browser auto-reconnects and replays
// `Last-Event-ID: live-game-42:0:17`; the next outcome you receive will
// be strictly after that point.
If you're consuming the stream with raw fetch (or any other client that doesn't track ids for you), pass it explicitly on reconnect — either as a Last-Event-ID header or as a lastEventId query parameter:
const res = await fetch(url, {
headers: {
"x-api-key": process.env.PROVABLE_API_KEY,
"Last-Event-ID": lastSeenOutcomeId, // e.g. "worker-1:0:42"
},
});
Authenticating the stream
The EventSource constructor doesn't let you set custom headers, so it can't carry an x-api-key header. Two practical workarounds:
- Server-side proxy — open the connection from your backend (where you can set the API key header) and re-broadcast the events to your users. Recommended for production.
- Polyfilled
EventSource— packages likeeventsourceon npm support custom headers and work in Node.js, so your backend can authenticate without any proxying.
Node.js example with an API key
Node 18+ ships a global fetch that returns a streaming body. That's enough to consume SSE without any extra dependency:
const res = await fetch(
"https://api.provable.io/api/stream?endpoint=ints&clientSeed=worker-1&count=10&min=1&max=6&intervalMs=500",
{ headers: { "x-api-key": process.env.PROVABLE_API_KEY } }
);
if (!res.ok) throw new Error(`stream failed: ${res.status}`);
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of res.body) {
buffer += decoder.decode(chunk, { stream: true });
let i;
while ((i = buffer.indexOf("\n\n")) !== -1) {
const frame = buffer.slice(0, i);
buffer = buffer.slice(i + 2);
if (!frame || frame.startsWith(":")) continue; // heartbeat / blank
const event = /^event:\s*(.+)$/m.exec(frame)?.[1] || "message";
const data = /^data:\s*(.+)$/m.exec(frame)?.[1] || "";
if (event === "outcome") {
const o = JSON.parse(data);
console.log("roll", o.outcomeId, o.outcome);
} else if (event === "done") {
console.log("done", JSON.parse(data));
return;
}
}
}
Lifecycle and billing
- The first outcome is generated synchronously during the handshake — bad parameters surface as an HTTP
400before any stream is opened. - Each outcome is one billed call on a live key. Test-mode keys (
pk_test_…) and anonymous streams aren't metered, but the initial connect still counts against the per-IP rate limit. - The server caps a single connection at 10 minutes. When it expires (or your daily quota is exhausted) you get a terminal
event: doneframe and the connection closes — just reconnect to keep going. - Calling
EventSource.close()(or letting the underlying TCP socket drop) tears down the server-side timers immediately, so no further outcomes are generated or billed.