This guide shows how to display real-time conversation events in a web application using Server-Sent Events (SSE) and React. See ConversationEvents schema for a detailed breakdown of the events and their payloads. Note that this example focuses on rendering the live transcript of the conversation as it happens, for a simplified example of processing the transcript of a finished conversation see Post-analysis example.

Getting the Events URL

When you create a conversation using POST /conversations (see Create a Conversation), the response includes:
  • channel.events_url - The events URL (without the ephemeral access token)
  • channel.token - Access token for authentication (can be supplied as a query parameter instead of the Authorization header with the API key)
The ephemeral access token allows temporary access to the events stream without requiring your API key.

Connecting to the Event Stream

Use the browser’s EventSource API to connect to the events endpoint:
const eventSource = new EventSource(eventsUrl);

eventSource.onmessage = (event) => {
  const eventData = JSON.parse(event.data);
  // Process the event
};

React Chat Log Example

When you connect to the real-time events stream using GET /conversations/{conversation_id}/events (see Real-time event stream), you receive a stream of events produced in the conversation by the agent and the user. You can also receive the same events over the websocket using WS /conversations/{conversation_id}/session (see Real-time chat session), in which you can also send the control messages to the agent. The raw events provide precise control over messages as they build up and tool calls as they are in-flight, making them ideal for implementing complex custom UIs with features like typing indicators, streaming text, and progress states. However, for simpler chat interfaces, you can render the conversation directly as the streaming chunks arrive without needing to track the detailed intermediate states. Here’s a complete example that renders a simple chat log. As events arrive, the chatEventsAccumulator builds up the conversation state by assembling incremental message chunks into a single agent message, and tool calls that are completed are collapsed into a single event. This approach gives you the best of both worlds - the real-time feel of streaming updates with the simplicity of a straightforward chat interface.
import React, { useState, useEffect } from 'react';

const ConversationEvents = ({ eventsUrl, token }) => {
  const [events, setEvents] = useState([]);

  useEffect(() => {
    const eventSource = new EventSource(eventsUrl + '?token=' + token);

    eventSource.onmessage = (event) => {
      const rawEvent = JSON.parse(event.data);
      setEvents(current => chatEventsAccumulator(current, rawEvent));
    };

    eventSource.onerror = (error) => {
      console.error('EventSource failed:', error);
    };

    return () => {
      eventSource.close();
    };
  }, [eventsUrl, token]);

  return (
    <div>
      <h3>Conversation Events</h3>
      {events.map((event) => (
        <div key={`${event.id}-${event.type}`}>
          {event.type === 'user.message' && (
            <div>
              <strong>User:</strong> {event.payload.text}
            </div>
          )}
          {event.type === 'agent.message' && (
            <div>
              <strong>Agent:</strong> {event.payload.text}
              {!event.payload.is_complete && <span> ...</span>}
            </div>
          )}
          {event.type === 'agent.tool.call' && (
            <div>
              <strong>Tool Call:</strong> {event.payload.name}({event.payload.arguments})
              {event.payload.result && <div>Result: {event.payload.result}</div>}
              {event.payload.error && <div>Error: {event.payload.error}</div>}
            </div>
          )}
          {event.type === 'conversation.started' && (
            <div><em>Conversation started</em></div>
          )}
          {event.type === 'conversation.ended' && (
            <div><em>Conversation ended</em></div>
          )}
          {event.type === 'server.error' && (
            <div><strong>Error:</strong> {event.payload.error}</div>
          )}
        </div>
      ))}
    </div>
  );
};

// Event accumulator function to handle streaming events
const chatEventsAccumulator = (current = [], rawEvent) => {
  const currentEvents = current || [];
  const { id, created_at, payload } = rawEvent;

  if (payload.type === "agent.message.delta") {
    const existingIndex = currentEvents.findIndex(
      (event) => event.type === "agent.message" && event.payload.generation_id === payload.generation_id
    );

    const simplifiedEvent = {
      id: existingIndex >= 0 ? currentEvents[existingIndex].id : id,
      created_at,
      type: "agent.message",
      payload: {
        generation_id: payload.generation_id,
        text: existingIndex >= 0
          ? (currentEvents[existingIndex].payload.text || "") + payload.text_delta
          : payload.text_delta || "",
        is_complete: false,
      },
    };

    if (existingIndex >= 0) {
      const newEvents = [...currentEvents];
      newEvents[existingIndex] = simplifiedEvent;
      return newEvents;
    } else {
      return [...currentEvents, simplifiedEvent];
    }
  }

  if (payload.type === "agent.message.completed") {
    const existingIndex = currentEvents.findIndex(
      (event) => event.type === "agent.message" && event.payload.generation_id === payload.generation_id
    );

    const simplifiedEvent = {
      id: existingIndex >= 0 ? currentEvents[existingIndex].id : id,
      created_at,
      type: "agent.message",
      payload: {
        generation_id: payload.generation_id,
        text: payload.text,
        is_complete: true,
      },
    };

    if (existingIndex >= 0) {
      const newEvents = [...currentEvents];
      newEvents[existingIndex] = simplifiedEvent;
      return newEvents;
    } else {
      return [...currentEvents, simplifiedEvent];
    }
  }

  if (payload.type === "agent.tool_call.created") {
    const simplifiedEvent = {
      id,
      created_at,
      type: "agent.tool.call",
      payload: {
        call_id: payload.call_id,
        name: payload.name,
        arguments: payload.arguments,
        is_complete: false,
      },
    };

    return [...currentEvents, simplifiedEvent];
  }

  if (payload.type === "agent.tool_call.returned") {
    const existingIndex = currentEvents.findIndex(
      (event) => event.type === "agent.tool.call" && event.payload.call_id === payload.call_id
    );

    const simplifiedEvent = {
      id: existingIndex >= 0 ? currentEvents[existingIndex].id : id,
      created_at,
      type: "agent.tool.call",
      payload: {
        call_id: payload.call_id,
        name: payload.name,
        arguments: payload.arguments,
        result: payload.result,
        error: payload.error,
        is_complete: true,
      },
    };

    if (existingIndex >= 0) {
      const newEvents = [...currentEvents];
      newEvents[existingIndex] = simplifiedEvent;
      return newEvents;
    } else {
      return [...currentEvents, simplifiedEvent];
    }
  }

  if (payload.type === "user.message.received") {
    const simplifiedEvent = {
      id,
      created_at,
      type: "user.message",
      payload: {
        text: payload.text,
        is_complete: true,
      },
    };
    return [...currentEvents, simplifiedEvent];
  }

  if (payload.type === "conversation.started") {
    const simplifiedEvent = {
      id,
      created_at,
      type: "conversation.started",
      payload: { is_complete: true },
    };
    return [...currentEvents, simplifiedEvent];
  }

  if (payload.type === "conversation.ended") {
    const simplifiedEvent = {
      id,
      created_at,
      type: "conversation.ended",
      payload: { is_complete: true },
    };
    return [...currentEvents, simplifiedEvent];
  }

  if (payload.type === "server.error") {
    const simplifiedEvent = {
      id,
      created_at,
      type: "server.error",
      payload: {
        error: payload.error,
        is_complete: true,
      },
    };
    return [...currentEvents, simplifiedEvent];
  }

  return currentEvents;
};

export default ConversationEvents;