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:

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

Next steps