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
| Option | Description |
|---|---|
after | Replay messages created after a message id or ISO-8601 timestamp; also used internally on resume |
reconnect | Re-open when the server closes the stream; default true; set to false to stop iteration on close |
maxRetries / max_retries | Maximum 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)