Guides

Streaming

New messages in a conversation can be streamed in real time over Server-Sent Events (SSE).

How the stream works

The flow has two steps.

First, the SDK calls the stream-ticket endpoint to obtain a single-use, short-lived ticket. The ticket expires after expires_in seconds.

Second, the SDK opens a GET to the message-stream endpoint with the ticket as a query parameter (?ticket=<ticket>) and Accept: text/event-stream. Authentication is carried by the ticket rather than the Authorization bearer key, so your API key never travels on the long-lived connection and the stream can safely be opened from a browser.

The server yields message events as new messages arrive and emits periodic heartbeat events to keep the connection open. The SDKs consume heartbeats internally and never surface them — the loop only ever sees real messages.

Each yielded value is a StreamMessageEvent with id, conversation_id, role, message, message_type, and created_at.

Reconnection

If the connection drops the SDK reconnects automatically. It tracks the id of the last message it delivered and resumes from that point using Last-Event-ID / the after query parameter, so messages are neither missed nor duplicated.

The reconnect budget counts consecutive failures and resets to zero every time a message is successfully delivered. A stream that has been running for hours and then blips still gets a full set of retries.

If too many concurrent streams are open for the team the server responds with too_many_streams and the SDK raises an error rather than reconnecting.

Reconnects use the same exponential backoff with full jitter as the rest of the retry system.

Stream options

OptionDescription
afterReplay messages created after a message id or ISO-8601 timestamp; also used internally on resume
reconnectRe-open when the server closes the stream; default true; set to false to stop iteration on close
maxRetries / max_retriesMaximum consecutive reconnect attempts before giving up; default 5

Using the SDK

stream() handles the ticket exchange and opens the SSE connection. Break out of the loop or abort the signal to stop.

const controller = new AbortController();

for await (const message of bimpe.conversations.messages.stream(agentId, conversationId, {
  signal: controller.signal,
})) {
  console.log(message.role, message.message);
}

The ticket step is exposed on its own when you want to open the stream yourself.

const { ticket, expires_in } = await bimpe.conversations.messages.streamTicket(agentId, conversationId);

stream() handles the ticket exchange and opens the SSE connection. Break out of the loop to stop.

for message in client.conversations.messages.stream(agent_id, conversation_id):
    print(message.role, message.message)

The async client returns an async iterator with the same options.

async for message in client.conversations.messages.stream(agent_id, conversation_id):
    print(message.role, message.message)

The ticket step is available on its own when you want to open the stream yourself.

ticket = client.conversations.messages.stream_ticket(agent_id, conversation_id)
print(ticket.ticket, ticket.expires_in)

On this page