Skip to main content

Streaming (Server-Sent Events)

Most Prezent operations are asynchronous — you POST /api/v1/autogenerations, receive a callback_id, and then check progress. You have three ways to do that:

MechanismBest for
Polling (GET /status)Low-volume, simple scripts.
WebhooksPush notifications into your backend at completion.
Streaming (SSE)you are hereReal-time progress updates inside an app, agent, or UI — every milestone as it happens.

Streaming complements the other two. Subscribe to the stream while a job is running and you'll see workflow.started, every task.completed, and finally workflow.completed or workflow.failed, with each event arriving within milliseconds of when the pipeline reaches it.

How it works in one diagram

1. POST /api/v1/autogenerations            →  { callback_id }    (existing)
2. POST /api/v1/streams/sessions → { stream_url, expires_at }
body: { callback_id }
3. new EventSource(stream_url) → live event stream

Step 2 is bearer-authenticated and runs through API Gateway; step 3 is served by a dedicated public host so the long-lived connection is unaffected by gateway timeouts.

Endpoint

POST /api/v1/streams/sessions

Creates a short-lived streaming URL for a callback_id that the same API key already owns. The URL is single-use-friendly but not single-use — it expires after 5 minutes (call this endpoint again to refresh).

Request body

{
"callback_id": "cb_01HZQ3Y7K3D6V0A0CS7HD3"
}

Response (201 Created)

{
"success": true,
"data": {
"session_id": "cb_01HZQ3Y7K3D6V0A0CS7HD3",
"stream_url": "https://stream-api.myprezent.com/v1/streams/cb_01HZQ3Y7K3D6V0A0CS7HD3?token=eyJhbGciOiJIUzI1NiIs...",
"expires_at": "2026-06-09T10:05:00Z"
}
}
FieldTypeDescription
session_idstringSame as the callback_id you passed in.
stream_urlstringFull URL to open with EventSource (or fetch + ReadableStream).
expires_atstring (ISO-8601 UTC)When the embedded token stops being accepted. Re-call POST /streams/sessions to mint a new URL.

Errors

CodeStatusMeaning
MISSING_REQUIRED_FIELD400Body did not include callback_id.
STREAM_CALLBACK_NOT_FOUND404No job with that callback_id was found, or it is not owned by the calling API key.

Consuming the stream

Open the stream_url with any standard SSE client. Browsers have EventSource built in; server-side languages use fetch + ReadableStream or an SSE library.

// 1. Kick off the job (existing API).
const { data: job } = await fetch('https://api.myprezent.com/api/v1/autogenerations', {
method: 'POST',
headers: {
Authorization: `Bearer ${PREZENT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt: 'Five-slide pitch on solar adoption' }),
}).then(r => r.json());

// 2. Ask for a streaming URL.
const { data: session } = await fetch('https://api.myprezent.com/api/v1/streams/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${PREZENT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ callback_id: job.callback_id }),
}).then(r => r.json());

// 3. Open the stream.
const es = new EventSource(session.stream_url);

es.addEventListener('workflow.started', e => {
console.log('started', JSON.parse(e.data));
});

es.addEventListener('task.completed', e => {
const evt = JSON.parse(e.data);
console.log(`task ${evt.data.task_name} done`);
});

es.addEventListener('workflow.completed', e => {
console.log('done', JSON.parse(e.data));
es.close();
});

es.addEventListener('workflow.failed', e => {
console.error('failed', JSON.parse(e.data));
es.close();
});

es.onerror = (err) => {
// Connection dropped. If you've stored the last `id:` you saw, you
// can refresh the URL and reconnect with ?last_event_id=N to resume
// without losing events.
console.warn('stream error', err);
};

Event catalog

Every frame on the wire is shaped like:

id: <integer monotonic per callback_id>
event: <event name>
data: { "type": "<event name>", "callback_id": "...", "ts": "...", "data": {...}, "seq": <integer> }

event nameWhendata payload includes
workflow.startedJob has been picked up and started executing.workflow_name, started_at
task.completedA pipeline subtask finished successfully.task_name, workflow_name, completed_at
workflow.completedThe entire job finished successfully. Stream closes after this event.workflow_name, completed_at, outputs_summary
workflow.failedThe job failed. Stream closes after this event.workflow_name, failed_at, error_log_preview

A typical stream looks like:

event: workflow.started
data: {"type":"workflow.started","callback_id":"cb_...","seq":1,...}

event: task.completed
data: {"type":"task.completed","callback_id":"cb_...","seq":2,...}

event: task.completed
data: {"type":"task.completed","callback_id":"cb_...","seq":3,...}

event: workflow.completed
data: {"type":"workflow.completed","callback_id":"cb_...","seq":4,...}

Reconnecting after a drop

If your connection drops mid-stream, refresh the URL and reconnect with ?last_event_id=N (where N is the id: of the last event you processed). The server will replay every event after N from a 30-minute window, then resume the live stream.

const es = new EventSource(`${session.stream_url}&last_event_id=${lastSeenId}`);

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

Keepalives

The server emits a :ping comment every 15 seconds. Standard SSE clients ignore comment lines; they're there to keep idle TCP connections alive through load balancers and proxies (we run a 4000-second idle timeout on our side).

Token lifecycle

The token in the stream_url is signed with HS256 and bound to your API key + the callback_id. It expires 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/v1/streams/sessions again for the same callback_id — you get a fresh URL with a new 5-minute window.

Tokens are intentionally short-lived so a leaked URL is useless after a coffee break.

Limits

  • One stream URL is bound to one callback_id. To stream another job, open another session.
  • 5-minute token TTL — call POST /streams/sessions again to refresh.
  • 30-minute event replay window — reconnects with ?last_event_id= must happen within 30 minutes of the job starting.
  • No per-second rate limit on the streaming connection itself; the session-create endpoint shares the standard per-API-key rate limits.

See also