> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trellis.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat Overview

> Understanding chat conversations with SSE and WebSocket streaming

The Trellis chat API enables natural language conversations with your databases. You can choose between two streaming protocols:

| Protocol      | Endpoint          | Best for                                         |
| ------------- | ----------------- | ------------------------------------------------ |
| **SSE**       | `POST /v1/chats`  | Simple integrations, one message at a time       |
| **WebSocket** | `WS /v1/chats/ws` | Real-time apps, multiple messages per connection |

Both protocols deliver the same events and provide identical functionality. Choose based on your application's needs.

## How it works

<Tabs>
  <Tab title="SSE (Server-Sent Events)">
    ```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
    sequenceDiagram
        participant App as Your Application
        participant API as Trellis API
        participant DB as Database
        
        App->>API: POST /v1/chats (message)
        API-->>App: SSE: chat_metadata
        API-->>App: SSE: processing
        API->>DB: Execute query
        DB-->>API: Results
        API-->>App: SSE: visualization (optional)
        API-->>App: SSE: text_delta (× N, streamed)
        API-->>App: SSE: message
    ```
  </Tab>

  <Tab title="WebSocket">
    ```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
    sequenceDiagram
        participant App as Your Application
        participant API as Trellis API
        participant DB as Database
        
        App->>API: Connect WS /v1/chats/ws
        API-->>App: authenticated
        App->>API: send_message
        API-->>App: chat_metadata
        API-->>App: processing
        API->>DB: Execute query
        DB-->>API: Results
        API-->>App: visualization (optional)
        API-->>App: text_delta (× N, streamed)
        API-->>App: message
        Note over App,API: Connection stays open for more messages
    ```
  </Tab>
</Tabs>

## Event Types

Both SSE and WebSocket deliver the same event types. The format differs slightly by protocol:

| Event           | Description                                                                                                                                                                                                                         |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chat_metadata` | Contains the chat ID (`id`) and the persisted user message ID (`user_message_id`). Emitted at the start of every response, including continued conversations and message edits.                                                     |
| `processing`    | Status updates while the AI is working. `data.status` is one of `starting`, `thinking`, `analyzing`, `processing`. Use to show loading indicators.                                                                                  |
| `visualization` | Sent when a chart or table is generated. Contains the **full structured payload** ready to render. See [Visualizations](/api-reference/chats/visualizations).                                                                       |
| `text_delta`    | An incremental chunk of the final assistant text, streamed as the model generates it. `data.content` is the delta (not the full text so far). Multiple frames arrive per turn. See [Streaming text deltas](#streaming-text-deltas). |
| `message`       | The final response from the AI. Always carries the **authoritative full content** plus the persisted message `id`. Use this as the canonical anchor — it is safe to replace any streaming buffer with `data.content` on receipt.    |
| `error`         | An error occurred. Payload: `error` (human message), `code` (stable enum), `retryable` (bool), `debug_id` (correlation ID), `error_type`. See [WebSocket → Error codes](/api-reference/chats/websocket#error-codes).                |

### Event format by protocol

<Tabs>
  <Tab title="SSE Format">
    SSE events are delivered as text with `event:` and `data:` lines:

    ```
    event: chat_metadata
    data: {"id": "chat_abc123", "user_message_id": "msg_001"}

    event: processing
    data: {"status": "thinking"}

    event: processing
    data: {"status": "analyzing"}

    event: visualization
    data: {"id": "ab12xy3456", "type": "chart", "chart_type": "bar", "title": "Orders by Region", "data": {"values": [{"label": "North", "value": 412}, {"label": "South", "value": 289}, {"label": "East", "value": 531}], "x_axis_label": "Region", "y_axis_label": "Orders"}}

    event: text_delta
    data: {"content": "There were 1,232 orders "}

    event: text_delta
    data: {"content": "placed last month, with the East "}

    event: text_delta
    data: {"content": "region leading at 531."}

    event: message
    data: {"content": "There were 1,232 orders placed last month, with the East region leading at 531.", "id": "msg_005"}
    ```
  </Tab>

  <Tab title="WebSocket Format">
    WebSocket events are delivered as JSON objects:

    ```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
    {"event": "chat_metadata", "data": {"id": "chat_abc123", "user_message_id": "msg_001"}}
    {"event": "processing", "data": {"status": "thinking"}}
    {"event": "processing", "data": {"status": "analyzing"}}
    {"event": "visualization", "data": {"id": "ab12xy3456", "type": "chart", "chart_type": "bar", "title": "Orders by Region", "data": {"values": [{"label": "North", "value": 412}, {"label": "South", "value": 289}, {"label": "East", "value": 531}], "x_axis_label": "Region", "y_axis_label": "Orders"}}}
    {"event": "text_delta", "data": {"content": "There were 1,232 orders "}}
    {"event": "text_delta", "data": {"content": "placed last month, with the East "}}
    {"event": "text_delta", "data": {"content": "region leading at 531."}}
    {"event": "message", "data": {"content": "There were 1,232 orders placed last month, with the East region leading at 531.", "id": "msg_005"}}
    ```
  </Tab>
</Tabs>

## Streaming text deltas

The final assistant text is streamed incrementally as the model generates it. Each `text_delta` event carries a **chunk** (`data.content`) — not the full text so far. Multiple frames arrive per turn; concatenating every `content` in order yields the full response.

The terminal `message` event always carries the authoritative full content plus the persisted message `id`. It is safe to treat `message.data.content` as the canonical text and overwrite any streaming buffer on receipt.

Typical handling pattern:

1. On the first `text_delta`, open an empty buffer for the in-progress message and hide any "thinking" indicator.
2. On subsequent `text_delta` events, append `data.content` to the buffer and re-render the UI.
3. On `message`, replace the buffer with `data.content` (the authoritative text) and associate the message with `data.id`.

<Note>
  If a client disconnects or sends `{"action": "interrupt"}` (WebSocket only) mid-stream, the partial text already delivered via `text_delta` frames is persisted as a regular assistant message. It surfaces in subsequent `GET /v1/chats/{id}` responses alongside fully completed messages.
</Note>

## Code Examples

<Tabs>
  <Tab title="SSE (JavaScript)">
    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    interface ChatEvent {
      type: "chat_metadata" | "processing" | "visualization" | "text_delta" | "message" | "error";
      data: any;
    }

    async function sendChatMessage(
      message: string,
      integrationId: string,
      token: string,
      onEvent: (event: ChatEvent) => void
    ): Promise<void> {
      const response = await fetch("https://api.trellis.sh/v1/chats", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
          Accept: "text/event-stream",
        },
        body: JSON.stringify({
          message,
          integration_id: integrationId,
        }),
      });

      const reader = response.body?.getReader();
      const decoder = new TextDecoder();
      let buffer = "";

      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split("\n");
        buffer = lines.pop() || "";

        let currentEvent = "";
        for (const line of lines) {
          if (line.startsWith("event: ")) {
            currentEvent = line.slice(7);
          } else if (line.startsWith("data: ") && currentEvent) {
            const data = JSON.parse(line.slice(6));
            onEvent({ type: currentEvent as ChatEvent["type"], data });
            currentEvent = "";
          }
        }
      }
    }

    // Handle each event type
    let streamingBuffer = "";
    sendChatMessage("Show me orders by region", "integration_id", "token", (event) => {
      switch (event.type) {
        case "chat_metadata":
          console.log("Chat ID:", event.data.id, "| User message:", event.data.user_message_id);
          break;
        case "processing":
          showLoadingIndicator(event.data.status);
          break;
        case "visualization":
          if (event.data.type === "table") {
            renderTable(event.data.headers, event.data.rows);
          } else if (event.data.type === "chart") {
            renderChart(event.data.chart_type, event.data.title, event.data.data);
          }
          break;
        case "text_delta":
          // Append the incremental chunk and re-render
          streamingBuffer += event.data.content;
          renderStreamingMessage(streamingBuffer);
          hideLoadingIndicator();
          break;
        case "message":
          // Replace buffer with authoritative full content + id
          displayMessage(event.data.content, event.data.id);
          streamingBuffer = "";
          hideLoadingIndicator();
          break;
        case "error":
          showError(event.data.error);
          break;
      }
    });
    ```
  </Tab>

  <Tab title="WebSocket (JavaScript)">
    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    interface ChatEvent {
      event: "chat_metadata" | "processing" | "visualization" | "text_delta" | "message" | "error";
      data: any;
    }

    class TrellisChat {
      private ws: WebSocket;
      private onEvent: (event: ChatEvent) => void;

      constructor(token: string, onEvent: (event: ChatEvent) => void) {
        this.onEvent = onEvent;
        this.ws = new WebSocket("wss://api.trellis.sh/v1/chats/ws");

        this.ws.onopen = () => {
          // Browser WebSocket cannot set headers — authenticate via first frame.
          this.ws.send(JSON.stringify({ action: "authenticate", token }));
        };

        this.ws.onmessage = (msg) => {
          const event = JSON.parse(msg.data);
          this.onEvent(event);
        };
      }

      sendMessage(message: string, chatId: string, integrationId: string) {
        // chat_id is required — obtain one from POST /v1/chats/create.
        this.ws.send(JSON.stringify({
          action: "send_message",
          message,
          chat_id: chatId,
          integration_id: integrationId,
        }));
      }

      close() {
        this.ws.close();
      }
    }

    // Usage
    let streamingBuffer = "";
    const chat = new TrellisChat("your_token", (event) => {
      switch (event.event) {
        case "chat_metadata":
          console.log("New chat:", event.data.id);
          break;
        case "processing":
          showLoadingIndicator();
          break;
        case "visualization":
          if (event.data.type === "table") {
            renderTable(event.data.headers, event.data.rows);
          } else if (event.data.type === "chart") {
            renderChart(event.data.chart_type, event.data.title, event.data.data);
          }
          break;
        case "text_delta":
          streamingBuffer += event.data.content;
          renderStreamingMessage(streamingBuffer);
          hideLoadingIndicator();
          break;
        case "message":
          displayMessage(event.data.content, event.data.id);
          streamingBuffer = "";
          break;
        case "error":
          showError(event.data.error);
          break;
      }
    });

    chat.sendMessage("Show me top 10 customers", "chat_abc123", "integration_id");
    ```
  </Tab>

  <Tab title="SSE (Python)">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import requests
    import json

    def send_chat_message(message: str, integration_id: str, token: str):
        """Send a chat message and stream the response via SSE."""
        
        response = requests.post(
            "https://api.trellis.sh/v1/chats",
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
                "Accept": "text/event-stream",
            },
            json={
                "message": message,
                "integration_id": integration_id,
            },
            stream=True
        )
        
        current_event = None
        streaming_buffer = ""

        for line in response.iter_lines(decode_unicode=True):
            if not line:
                continue
                
            if line.startswith("event: "):
                current_event = line[7:]
            elif line.startswith("data: ") and current_event:
                data = json.loads(line[6:])

                if current_event == "visualization":
                    if data["type"] == "table":
                        handle_table(data["headers"], data["rows"])
                    elif data["type"] == "chart":
                        handle_chart(data["chart_type"], data.get("title"), data["data"])
                elif current_event == "text_delta":
                    # Append the incremental chunk
                    streaming_buffer += data["content"]
                elif current_event == "message":
                    # Replace buffer with authoritative full content
                    print(data["content"])
                    streaming_buffer = ""

                yield {"type": current_event, "data": data}
                current_event = None


    # Usage
    for event in send_chat_message("Show me revenue by month", "integration_id", "token"):
        print(f"{event['type']}: {event['data']}")
    ```
  </Tab>

  <Tab title="WebSocket (Python)">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import asyncio
    import json
    import websockets

    async def chat_with_websocket(token: str, chat_id: str, integration_id: str, messages: list[str]):
        """Send multiple messages over a persistent WebSocket connection."""

        uri = "wss://api.trellis.sh/v1/chats/ws"

        async with websockets.connect(
            uri,
            additional_headers=[("Authorization", f"Bearer {token}")],
        ) as ws:
            # Wait for authentication confirmation
            auth_response = await ws.recv()
            auth_data = json.loads(auth_response)

            if auth_data["event"] != "authenticated":
                raise Exception("Authentication failed")

            for message in messages:
                # Send message — chat_id is required
                await ws.send(json.dumps({
                    "action": "send_message",
                    "message": message,
                    "chat_id": chat_id,
                    "integration_id": integration_id,
                }))

                # Receive events until we get the final message
                streaming_buffer = ""
                while True:
                    response = await ws.recv()
                    event = json.loads(response)

                    if event["event"] == "visualization":
                        data = event["data"]
                        if data["type"] == "table":
                            handle_table(data["headers"], data["rows"])
                        elif data["type"] == "chart":
                            handle_chart(data["chart_type"], data.get("title"), data["data"])
                    elif event["event"] == "text_delta":
                        # Append the incremental chunk
                        streaming_buffer += event["data"]["content"]
                    elif event["event"] == "message":
                        # Authoritative full content
                        print(event["data"]["content"])
                        break
                    elif event["event"] == "error":
                        print("Error:", event["data"]["error"])
                        break


    # Usage — obtain chat_id first via POST /v1/chats/create
    asyncio.run(chat_with_websocket(
        "your_token",
        "chat_abc123",
        "integration_id",
        ["How many orders last month?", "Break it down by region"]
    ))
    ```
  </Tab>
</Tabs>

## Chat lifecycle

1. **Create a chat** - Call [`POST /v1/chats/create`](/api-reference/chats/create-chat) and keep the returned `id`. This is required before sending messages or uploading files.
2. **(Optional) Attach files** - Upload files via [`POST /v1/chats/uploads`](/api-reference/chats/upload-files) with the `chat_id`, then reference their IDs with the `upload_ids` field in subsequent messages.
3. **Send a message** - `POST /v1/chats` (SSE) or the [WebSocket endpoint](/api-reference/chats/websocket), passing the `chat_id`. The `chat_metadata` event echoes the same `id` plus a persisted `user_message_id`.
4. **Continue the conversation** - Reuse the same `chat_id` on every follow-up turn.
5. **View history** - Use `GET /v1/chats/{id}` to retrieve all messages (upload metadata is enriched with fresh download URLs).
6. **Edit messages** - Use `PATCH /v1/chats/{id}/messages/{id}` to edit a user message and re-stream the response.
7. **Manage chats** - Update titles, delete chats, remove uploads, or provide feedback on messages.

<Note>
  Lazy chat creation has been retired. Sending `POST /v1/chats` or `POST /v1/chats/uploads` without a `chat_id` returns `400 MISSING_CHAT_ID`; passing an unknown ID returns `404 CHAT_NOT_FOUND`. Always call [Create Chat](/api-reference/chats/create-chat) first.
</Note>

## Rate limits

The chat surface enforces per-JWT throttles to protect the platform from runaway clients. All limits return `429` (HTTP) or a `RATE_LIMIT` `error` event (WebSocket) with a `Retry-After` header / `retry_after_seconds` field.

| Endpoint                         | Limit                                                                                                          |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `POST /v1/chats/create`          | 20 / minute                                                                                                    |
| `POST /v1/chats/uploads`         | 30 / minute                                                                                                    |
| `POST /v1/chats/followups`       | 10 / minute, 5 / 10 seconds (burst)                                                                            |
| WebSocket `send_message` frame   | 10 / minute, 3 / 10 seconds (burst) per socket — with the 3-connection cap, the per-JWT ceiling is 30 / minute |
| WebSocket concurrent connections | 3 per JWT (4th is closed with code `4429`)                                                                     |

Each JWT has its own bucket — re-authenticating returns a fresh `jti` claim and a fresh set of buckets.

## Message roles

Messages in a chat have one of two roles:

| Role        | Description               |
| ----------- | ------------------------- |
| `user`      | Messages sent by the user |
| `assistant` | Responses from the AI     |
