Skip to main content
The concierge delivers new messages live over Server-Sent Events (SSE) at GET /v1/conversations/{id}/stream. Because the response is an open text/event-stream rather than a single request/response, it is documented here as a concept rather than in the interactive reference - the “Try it” playground cannot represent a long-lived stream.
Streaming is member-mode: it uses the member token, same as the rest of the concierge. See Conversations for the request/response message operations.

Consuming the stream

Each SDK exposes the stream as the idiomatic async sequence for its language, so you consume new messages with a normal loop:
LanguageTypeConsume with
TypeScriptAsyncIterable<Message>for await (const message of ...)
SwiftAsyncStream<Message>for try await message in ...
KotlinFlow<Message>.collect { message -> ... }
A common pattern is to print the recent backlog with listMessages first, then live-stream what follows:
import { initialize } from "@expys/sdk";

const token = process.env.EXPYS_MEMBER_TOKEN;
if (!token) {
  throw new Error(
    "Set EXPYS_MEMBER_TOKEN (a member token from your backend's /v1/auth/exchange)",
  );
}

const conversationId = process.env.EXPYS_CONVERSATION_ID;
if (!conversationId) {
  throw new Error("Set EXPYS_CONVERSATION_ID (a conversation to stream)");
}
// Narrow for use inside main() (a module-level const isn't narrowed across it).
const cnv: string = conversationId;

const expys = initialize({
  baseUrl: process.env.EXPYS_BASE_URL,
  environment: "sandbox",
  token,
});

async function main(): Promise<void> {
  // Optional: print the recent backlog first, then live-stream what follows.
  const { messages } = await expys.listMessages(cnv, { limit: 20 });
  for (const message of messages) {
    console.log(`[history ${message.authorID}] ${message.body ?? "(no body)"}`);
  }

  let received = 0;
  console.log("listening for new messages (stops after 5)...");

  // `for await` consumes the AsyncIterable lazily. Breaking the loop (here, after
  // five messages) tears down the underlying HTTP connection and any reconnect
  // timer - no leaked sockets. In a real app you would break on a signal /
  // unmount instead of a fixed count.
  for await (const message of expys.streamMessages(cnv)) {
    received += 1;
    console.log(`[live ${message.authorID}] ${message.body ?? "(no body)"}`);
    if (received >= 5) {
      break; // closes the connection
    }
  }

  console.log(`done: received ${received} live message(s)`);
}

Reconnect and backoff

The SDKs reconnect automatically if the stream drops, using the same full-jitter backoff as the rest of the client (base 500ms, capped at 10s). You consume one continuous sequence of messages; transient disconnects are handled underneath.

Cancellation

Stopping consumption tears down the underlying HTTP connection and any pending reconnect timer - no leaked sockets:
break out of the for await loop (or return), and the connection closes.
In a real app, end the stream on a lifecycle signal - a screen unmount, a cancelled task, or a closed scope - rather than after a fixed number of messages as the example does.