Connect via WebSocket for real-time, bidirectional chat communication. WebSocket is ideal for applications that need to send multiple messages without reconnecting.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.
For event types and general chat concepts, see the Chat Overview.
Connection
Connect to the WebSocket endpoint at:Authorization: Bearer header on the upgrade request, or via a first-message authenticate frame for browser clients.
Server-side keepalive (opt-in)
Long-running tool calls — for example a multi-query analytics tool that takes a minute to complete — can leave the WebSocket silent for tens of seconds while the server works. Some load balancers and mobile clients close idle connections in that window. To prevent that, append?heartbeat=1 to the connection URL. The server will then emit a small heartbeat event every 15 s while a turn is streaming, which keeps the connection lit on every hop:
Heartbeats are opt-in today and may become the default in a future release. Clients with strict event-type validation should add explicit handling before enabling the flag.
Authentication
- First message (browser)
Concurrent connections
Each JWT may hold at most 3 concurrent WebSocket connections. A 4th connection receives aWS_CONNECTION_CAP error frame and is closed with code 4429:
jti claim).
Frame rate limits
Each socket may send at most 10send_message frames per minute with a 3-frame burst over 10 seconds. Excess frames receive an error event:
send_message is 30 per minute. Other actions (ping, interrupt, delete_message, edit_message) are not throttled.
Sending Messages
After authentication, send chat messages using thesend_message action:
Must be
"send_message".The natural language message to send.
ID of the database integration to query. Required for database queries.
ID of an existing chat. Obtain one from Create Chat. Omitting this field returns an
error event with code: "MISSING_CHAT_ID"; passing an ID that doesn’t belong to the caller returns code: "CHAT_NOT_FOUND". The connection stays open in both cases.Optional title hint for the chat. If omitted, the chat keeps the title set at creation time.
AI model to use for this message. If omitted, the default model is used. See List Models for available values.
List of upload IDs to attach to this message. Upload files first via Upload Files, then reference their IDs here. Requires
chat_id to be set.If
true, emit tool_call and tool_result events for real-time tool execution visibility. Default: false.Example message
Example with file attachment
Response Events
Events are delivered as JSON objects withevent and data fields:
| Event | Data | Description |
|---|---|---|
authenticated | {"status": "ok"} | Connection authenticated successfully |
chat_metadata | {"id": "chat_...", "user_message_id": "msg_..."} | Chat ID and persisted user message ID. Emitted at the start of every response. |
processing | {"status": "starting" | "thinking" | "analyzing" | "processing"} | AI processing status. starting is emitted within ~100ms of request receipt so the UI can render a spinner before agent startup completes. |
tool_call | {"tool": "...", "args": {...}} | Tool invocation started (only if include_tool_events is true) |
tool_result | {...} | Tool execution completed (only if include_tool_events is true) |
visualization | Full chart or table payload | A chart or table was generated. See Visualizations. |
text_delta | {"content": "..."} | An incremental chunk of the final assistant text, streamed as the model generates it. Multiple frames arrive per turn. Concatenate content values in order to build the response. See Chat Overview → Streaming text deltas. |
message | {"content": "...", "id": "msg_..."} | The final response. Always carries the authoritative full content plus the persisted message id. Safe to overwrite any streaming buffer with content on receipt. |
interrupted | {"status": "interrupted"} | The in-progress response was cancelled by an interrupt action |
error | {"error": "...", "code": "...", "retryable": boolean, "debug_id": "...", "error_type": "..."} | An error occurred. See Error codes for the stable code enum and retry semantics. |
pong | {} | Response to ping action |
heartbeat | {"ts": <unix-seconds float>} | Server-initiated keepalive emitted every 15 s while a turn is streaming. Treat as a no-op. Only emitted when the WebSocket is opened with ?heartbeat=1 — see Server-side keepalive. |
deleted | {"message_id": "msg_..."} | A message was successfully deleted via delete_message |
Example response stream
Interrupting a Response
Send an interrupt action while the AI is streaming a response to cancel it:Any assistant text already streamed via
text_delta frames before the interrupt is persisted as a regular assistant message and appears in subsequent GET /v1/chats/{id} responses. Your client’s streaming buffer at the moment of interrupt is a faithful preview of what got saved.Editing Messages
Edit a previously sent user message. This truncates all messages after the edited one and re-streams a new response.chat_metadata followed by a new response stream, just like send_message.
Deleting Messages
Delete a message and all subsequent messages in the conversation:Keep-Alive
Send periodic pings to keep the connection alive:Interrupting a Response
Send aninterrupt action while the AI is streaming to cancel the in-progress response:
interrupt is sent, an error event is returned instead.
For SSE connections, interruption is handled automatically when the HTTP connection is closed — no special action required.
Managing Messages
Delete a message
Send adelete_message action to remove a message and all subsequent messages from the chat. This is irreversible.
Must be
"delete_message".ID of the message to delete. The target message and every message after it are removed.
Edit a message
Send anedit_message action to replace a user message’s content. All messages after the edited message are removed and a fresh agent response is streamed back.
Only
user messages can be edited. Attempting to edit an assistant message returns an error event.Must be
"edit_message".ID of the user message to edit.
The new message content to replace the original.
ID of the database integration to query for the re-streamed response.
AI model to use for the re-streamed response.
If
true, internal tool events are included in the stream. Defaults to false.chat_metadata:
Error Handling
Errors are delivered as events, not connection closures (unless authentication fails):error string is human-readable and may change. Branch on code for retry logic. The debug_id is an 8-hex-char correlation ID — include it when reporting issues to support.
Connection close codes
| Code | Reason |
|---|---|
4001 | Authentication required or invalid token |
4002 | Invalid JSON message |
4400 | JWT was passed in the URL (?token=). In practice the upgrade is rejected with HTTP 403 before the WebSocket exists, so this code is rarely observed. |
4429 | Concurrent-connection cap (3 per JWT) reached |
1011 | Internal server error |
Error codes
Thecode field on error events is a stable enum. Branch on it for retry logic rather than string-matching the error text. New codes may be added — treat unknown codes as non-retryable.
| Code | Retryable | Typical cause |
|---|---|---|
INTEGRATION_NOT_FOUND | no | Datasource ID does not exist for this organization |
INTEGRATION_NOT_CONFIGURED | no | Datasource is missing required configuration |
DATABASE_CONNECTION_FAILED | yes | Network or connection-pool failure reaching the datasource |
DATABASE_QUERY_FAILED | no | SQL syntax error, missing column, or constraint violation |
SERIALIZATION_ERROR | no | A value in the payload is not JSON-serializable |
VALIDATION_ERROR | no | Invalid input (message content, parameters, IDs) |
ACCESS_DENIED | no | Caller lacks permission for the requested resource |
MISSING_CHAT_ID | no | send_message or edit_message was sent without chat_id. Create one via Create Chat. |
CHAT_NOT_FOUND | no | Provided chat_id does not belong to the caller. The connection stays open. |
WS_CONNECTION_CAP | no | The 3-concurrent-connection cap for this JWT was reached; the connection is closed with code 4429. |
RATE_LIMIT | yes | Either the per-socket send_message frame throttle (10/min + 3/10s burst) or an upstream LLM rate limit. The payload includes retry_after_seconds for the frame throttle. |
MODEL_ERROR | yes | Upstream LLM (Anthropic / OpenAI / Google) returned an error |
INTERNAL_ERROR | yes | Unclassified failure — check debug_id and retry with backoff |
AGENT_RETRY_EXHAUSTED | yes | The model produced invalid tool arguments repeatedly and the agent exhausted its retry budget. Often resolved by rephrasing the request or retrying with a different model. |