Skip to main content

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.

The Trellis chat API enables natural language conversations with your databases. You can choose between two streaming protocols:
ProtocolEndpointBest for
SSEPOST /v1/chatsSimple integrations, one message at a time
WebSocketWS /v1/chats/wsReal-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

Event Types

Both SSE and WebSocket deliver the same event types. The format differs slightly by protocol:
EventDescription
chat_metadataContains 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.
processingStatus updates while the AI is working. data.status is one of starting, thinking, analyzing, processing. Use to show loading indicators.
visualizationSent when a chart or table is generated. Contains the full structured payload ready to render. See Visualizations.
text_deltaAn 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.
messageThe 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.
errorAn error occurred. Payload: error (human message), code (stable enum), retryable (bool), debug_id (correlation ID), error_type. See WebSocket → Error codes.

Event format by protocol

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"}

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.
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.

Code Examples

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;
  }
});

Chat lifecycle

  1. Create a chat - Call POST /v1/chats/create 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 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, 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.
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 first.

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.
EndpointLimit
POST /v1/chats/create20 / minute
POST /v1/chats/uploads30 / minute
POST /v1/chats/followups10 / minute, 5 / 10 seconds (burst)
WebSocket send_message frame10 / minute, 3 / 10 seconds (burst) per socket — with the 3-connection cap, the per-JWT ceiling is 30 / minute
WebSocket concurrent connections3 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:
RoleDescription
userMessages sent by the user
assistantResponses from the AI