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:
| 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
SSE (Server-Sent Events)
WebSocket
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. |
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. |
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. |
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:
- On the first
text_delta, open an empty buffer for the in-progress message and hide any “thinking” indicator.
- On subsequent
text_delta events, append data.content to the buffer and re-render the UI.
- 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
SSE (JavaScript)
WebSocket (JavaScript)
SSE (Python)
WebSocket (Python)
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;
}
});
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");
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']}")
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"]
))
Chat lifecycle
- Create a chat - Call
POST /v1/chats/create and keep the returned id. This is required before sending messages or uploading files.
- (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.
- 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.
- Continue the conversation - Reuse the same
chat_id on every follow-up turn.
- View history - Use
GET /v1/chats/{id} to retrieve all messages (upload metadata is enriched with fresh download URLs).
- Edit messages - Use
PATCH /v1/chats/{id}/messages/{id} to edit a user message and re-stream the response.
- 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.
| 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 |