Skip to main content
Version: 2.0

Streaming (Server-Sent Events)

In API 2.0, streaming is not an alternative to polling — it is how the AutoGenerator delivers results. Generate, edit, and download all return a stream, and the finished presentation's URL arrives on the stream's terminal event.

This page covers the stream itself: the event contract, how to consume it, reconnection, and limits. For the endpoints that mint a stream, see the AI Builder API.

Looking for the 1.0 streaming API?

In 1.0, streaming was optional — an alternative to polling GET /api/v1/autogenerator/status, using POST /api/v1/streams/sessions and a workflow.* / task.* event catalog. That contract is unchanged and documented under version 1.0.

The v2 stream is a different, simpler contract: one event shape, and no status endpoint to fall back on.

How it works

1. POST /api/v2/generate            →  { resData: { callback_id } }
2. POST /api/v2/streams/sessions → { data: { stream_url } }
body: { callback_id }
3. open stream_url (SSE) → progress events…
terminal event + download_url

Step 2 is Bearer-authenticated through API Gateway. Step 3 is served by a dedicated host (stream-api-<env>.myprezent.com) so the long-lived connection is not subject to gateway timeouts.

POST /api/v2/autogenerator/download skips step 2 — it returns a stream_url directly.

The token is embedded in stream_url, so you send no Authorization header when opening it. Browsers' EventSource cannot set headers at all, which is why the token travels in the URL.

The event contract

Every event — progress and terminal alike — has the same four-field shape, so a single parser handles the whole stream:

id: 7
event: status
data: {"status":"inprogress","message":"Analyzing the key elements","callback_id":"3f8e7d61-…"}

FieldTypeNotes
statusstringinprogress while working; then exactly one of success, failed, cancelled.
messagestringHuman-readable step description — written for display, safe to show a user verbatim. On a failed event, the reason.
callback_idstringThe generation or edit turn this event belongs to.
download_urlstring | nullOnly on a success event. The finished file.

The SSE event: name is status for progress events and done, error, or cancelled for the terminal one. Branch on the status field, not the event name — it carries the same information with one less special case.

The stream closes after the terminal event.

Progress messages

Progress messages come from a fixed set, in pipeline order:

#MessageWhen
1Understanding your promptAlways
2Checking for app referencesOnly when the prompt references a connected app
3Analyzing the key elementsAlways
4Adjusting structure for clarityAlways
5Optimizing spacing and alignmentAlways
6Crafting your speaker notesOnly when speaker notes were requested
7Finalizing your slidesAlways

Steps that do not apply to your request are skipped, and each message is sent at most once per turn. Do not treat this as a fixed-length progress bar — drive your UI off the messages themselves, or off an indeterminate spinner.

Connecting to an app may substitute a more specific message (for example "Fetching data from Google Drive"), and app-related problems surface as additional notice events that are still inprogress — they inform, they do not end the stream.

A typical stream

event: status
data: {"status":"inprogress","message":"Understanding your prompt","callback_id":"3f8e…"}

event: status
data: {"status":"inprogress","message":"Analyzing the key elements","callback_id":"3f8e…"}

event: status
data: {"status":"inprogress","message":"Optimizing spacing and alignment","callback_id":"3f8e…"}

event: status
data: {"status":"inprogress","message":"Finalizing your slides","callback_id":"3f8e…"}

event: done
data: {"status":"success","message":"Your presentation is ready","callback_id":"3f8e…","download_url":null}
download_url can be null on a success

On a generation stream, a null download_url means the deck was built but not yet exported to a file. That is a success, not a failure — call POST /api/v2/autogenerator/download to produce the file, then read that stream.

On a download stream, download_url is always populated.

Consuming the stream

Open stream_url with any standard SSE client. Browsers have EventSource built in; server-side, use an SSE library or parse the frames yourself.

// Node: npm install eventsource  •  Browser: EventSource is built in
import { EventSource } from 'eventsource';

const BASE = 'https://api.prezent.ai';
const HEADERS = {
Authorization: `Bearer ${PREZENT_API_KEY}`,
'Content-Type': 'application/json',
};

// 1. Start the generation.
const gen = await fetch(`${BASE}/api/v2/generate`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ prompt: 'Five-slide pitch on solar adoption' }),
}).then((r) => r.json());

const callbackId = gen.resData.callback_id;

// 2. Mint a stream URL.
const session = await fetch(`${BASE}/api/v2/streams/sessions`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ callback_id: callbackId }),
}).then((r) => r.json());

// 3. Read it. No Authorization header — the token is in the URL.
const downloadUrl = await new Promise((resolve, reject) => {
const es = new EventSource(session.data.stream_url);

es.onmessage = (e) => {
const event = JSON.parse(e.data);

if (event.status === 'inprogress') {
// Safe to show the user directly.
console.log(event.message);
return;
}

es.close();
if (event.status === 'success') resolve(event.download_url ?? null);
else reject(new Error(`${event.status}: ${event.message}`));
};

es.onerror = () => { es.close(); reject(new Error('stream failed')); };
});

console.log('done:', downloadUrl);

onmessage receives every event regardless of its SSE event: name, so one handler covers progress and terminal events alike.

Reconnecting after a drop

Each event carries a monotonic id:. If the connection drops, reconnect with ?last_event_id=N — where N is the last id: you processed — and the server replays everything after N, then resumes live:

const es = new EventSource(`${streamUrl}&last_event_id=${lastSeenId}`);

Most browser EventSource implementations send a Last-Event-ID header automatically on reconnect; that is accepted too.

If the token has expired by the time you reconnect, mint a fresh URL with POST /api/v2/streams/sessions — the new stream replays from the start of the turn, so a late reconnect without a stored last_event_id still loses nothing.

For an edit, a freshly minted URL automatically replays from the point the edit was queued, so you see the whole edit turn even if you connect a moment late.

Keepalives

The server emits a :ping comment every 15 seconds. Standard SSE clients ignore comment lines — they exist to keep idle connections alive through load balancers and proxies.

Set no read timeout on the connection. A generation is legitimately quiet for stretches, and the pings are what distinguish "still working" from "dead". Both client examples above set the timeout to none for exactly this reason.

Token lifecycle

The token in stream_url is signed and bound to your API key and the presentation. It expires about 5 minutes after issue. You can:

  • Reuse the same URL within those 5 minutes for any number of reconnect attempts.
  • Refresh by calling POST /api/v2/streams/sessions again for the same callback_id — a fresh URL with a new window, replaying from the start of the turn.

Once open, a connection is not cut short by the token expiring — the 5-minute window governs how long the URL can be used to open a stream.

Tokens are deliberately short-lived so a leaked URL is useless quickly.

Limits

Stream URL scopeOne URL is bound to one callback_id. Stream another presentation with another session.
Token TTL~5 minutes. Call POST /api/v2/streams/sessions again to refresh.
Event replay window~2 hours from the start of the turn. Reconnects with ?last_event_id= must fall inside it.
callback_id lifetime~3 hours, after which no new stream can be minted for it.
Rate limitsThe streaming connection itself is not rate-limited per second; the session-create endpoint shares the standard per-key limits.
ConcurrencyOne turn runs at a time per presentation — an edit is rejected while a previous turn is still running.

See also