Skip to main content
ODAI’s primary interaction surface is a WebSocket endpoint that delivers streaming text, tool call events, agent handoff events, and suggested prompts as a chat session progresses.

Endpoint

WSS /chats/{chat_id}?token={auth_token}

Parameters

chat_id
string
required
Unique identifier for the chat session. Use a UUID or any client-generated string. If the chat does not exist it is created automatically; if it does exist the conversation history is resumed.
token
string
required
Firebase ID token for the authenticated user. The token is validated server-side on connection. The connection is closed immediately if the token is invalid or expired.

Connection lifecycle

1

Obtain a Firebase ID token

Sign in with Firebase Authentication on the client and retrieve the current user’s ID token.
const token = await firebase.auth().currentUser.getIdToken();
2

Open the WebSocket

Connect to the endpoint, appending the token as a query parameter.
const chatId = crypto.randomUUID();
const ws = new WebSocket(
  `wss://api.odai.com/chats/${chatId}?token=${token}`
);
3

Send a message

Once the connection is open, send a JSON-encoded message object.
ws.addEventListener("open", () => {
  ws.send(JSON.stringify({
    message: "What's my Gmail inbox?",
    thread_id: crypto.randomUUID()
  }));
});
4

Receive streaming events

Listen for incoming messages. Each message is a JSON object with a type field that identifies the event. See Response event types below.
ws.addEventListener("message", (event) => {
  const data = JSON.parse(event.data);
  handleEvent(data);
});
5

Disconnect

Close the socket when the session ends. The server removes the connection from its active pool automatically.
ws.close();

Request message format

Every message you send must be a JSON string with the following shape:
{
  "message": "User prompt text",
  "thread_id": "unique_thread_identifier"
}
message
string
required
The natural language prompt from the user. The orchestrator parses this text to decide which agents to invoke.
thread_id
string
required
A client-generated identifier for this specific message turn. Use a UUID. This is tracked for analytics and is included in stored chat responses.

Response event types

The server sends multiple JSON messages per user prompt. Each message has a type field.

raw_response_event — text delta

Streamed character chunk from the active agent. Clients should append delta to a text buffer to reconstruct the full response.
{
  "type": "raw_response_event",
  "delta": "Here are your ",
  "current_agent": "GMail"
}

tool_call

Emitted when the agent invokes a tool. Use this to show a progress indicator to the user.
{
  "type": "tool_call",
  "name": "fetch_google_email_inbox",
  "description": "Fetching Inbox...",
  "current_agent": "GMail"
}

tool_output

The structured result returned by a tool. Only sent when the tool’s display_response flag is true. The output field contains the full ToolResponse payload.
{
  "type": "tool_output",
  "output": {
    "response_type": "google_email_inbox",
    "agent_name": "GMail",
    "friendly_name": "GMAIL Inbox",
    "display_response": true,
    "response": [...]
  },
  "current_agent": "GMail"
}

agent_updated

Emitted when control transfers from the orchestrator to a specialized agent.
{
  "type": "agent_updated",
  "new_agent": "GMail",
  "current_agent": "GMail",
  "name": "GMail"
}

handoff

Emitted when a handoff function call is detected in the event stream.
{
  "type": "handoff",
  "name": "transfer_to_gmail",
  "current_agent": "ODAI"
}

llm_response

The complete text response from the current agent for this turn. Sent after all text deltas have been streamed.
{
  "type": "llm_response",
  "current_agent": "GMail",
  "response": "Here are your 10 most recent inbox messages: ..."
}

suggested_prompts

Sent after end_of_stream. Contains follow-up prompt suggestions generated from the conversation context.
{
  "type": "suggested_prompts",
  "prompts": [
    "Reply to the message from Alice",
    "Search for emails about the invoice",
    "Send an email to Bob"
  ]
}

end_of_stream

Signals that the agent has finished processing the current user message. After receiving this event, no further response events will arrive for this turn (except suggested_prompts).
{
  "type": "end_of_stream"
}

Complete JavaScript example

async function startChat() {
  // 1. Get Firebase ID token
  const token = await firebase.auth().currentUser.getIdToken(/* forceRefresh */ true);
  const chatId = crypto.randomUUID();

  // 2. Open WebSocket connection
  const ws = new WebSocket(`wss://api.odai.com/chats/${chatId}?token=${token}`);

  let responseBuffer = "";

  ws.addEventListener("open", () => {
    console.log("Connected");

    // 3. Send a message
    ws.send(JSON.stringify({
      message: "What are my recent Yelp-worthy restaurants in SF?",
      thread_id: crypto.randomUUID()
    }));
  });

  ws.addEventListener("message", (event) => {
    const data = JSON.parse(event.data);

    switch (data.type) {
      case "agent_updated":
        console.log(`Agent: ${data.new_agent}`);
        break;

      case "tool_call":
        console.log(`Tool: ${data.description}`);
        break;

      case "raw_response_event":
        responseBuffer += data.delta;
        process.stdout.write(data.delta); // stream to UI
        break;

      case "tool_output":
        console.log("Tool output:", data.output.response_type);
        renderToolOutput(data.output);
        break;

      case "end_of_stream":
        console.log("\n[Done]");
        break;

      case "suggested_prompts":
        renderSuggestedPrompts(data.prompts);
        break;
    }
  });

  ws.addEventListener("close", (event) => {
    console.log("Disconnected:", event.code, event.reason);
  });

  ws.addEventListener("error", (error) => {
    console.error("WebSocket error:", error);
  });
}

Reconnection handling

The server does not automatically reconnect on drop. Implement exponential backoff on the client:
function connectWithRetry(chatId, getToken, attempt = 0) {
  const delay = Math.min(1000 * 2 ** attempt, 30000);

  setTimeout(async () => {
    try {
      const token = await getToken();
      const ws = new WebSocket(
        `wss://api.odai.com/chats/${chatId}?token=${token}`
      );

      ws.addEventListener("open", () => {
        console.log("Reconnected");
        attempt = 0; // reset on success
      });

      ws.addEventListener("close", () => {
        connectWithRetry(chatId, getToken, attempt + 1);
      });
    } catch (err) {
      connectWithRetry(chatId, getToken, attempt + 1);
    }
  }, delay);
}
Always fetch a fresh Firebase ID token before reconnecting. Tokens expire after one hour and a stale token will cause the server to reject the new connection.

Build docs developers (and LLMs) love