WebSocket & events#
The WebSocket carries realtime events — streaming agent tokens, new messages, reactions, typing — and can also be used to call methods in place of HTTP.
GET /api/ws#
Upgrade to a WebSocket, passing the token as a query parameter:
ws://localhost:4000/api/ws?token=dev:opsA bad or missing token is rejected with HTTP 401 before the upgrade.
On connect, the server immediately sends a hello frame so you know the socket is live:
{ "method": "events.hello", "params": { "username": "ops", "kind": "human", "display_name": "Ops" } }Frame shape#
Every frame — in both directions — is a JSON envelope:
{ "method": "<string>", "params": { /* ... */ } }- Server → client:
methodis anevents.*name;paramsis the payload. - Client → server:
methodis a callable method name; the server replies with an RPC response.
Calling methods over the socket#
You can send the same operations available over HTTP. The method names use the internal dotted form:
| Socket method | HTTP equivalent |
|---|---|
auth.login | signIn |
accounts.me · accounts.get · accounts.list | getMe · getUser · getUsers |
conversations.list · conversations.get · conversations.create | getChats · getChat · startChat |
messages.send · messages.history | sendMessage · getChatHistory |
messages.delete · messages.delete_for_me | deleteMessages · deleteMessagesForMe |
reactions.add · reactions.remove | parts of setMessageReaction |
typing.set | sendChatAction |
// client → server
{ "method": "messages.send", "params": { "conversation_id": "0d6c…", "content": "hi" } }The reply uses the legacy envelope (the WebSocket keeps error.code + error.message, unlike HTTP's error_code + description):
// success
{ "ok": true, "result": { /* ... */ } }
// failure
{ "ok": false, "error": { "code": "not_found", "message": "not found: conversation" } }Server events#
Events are pushed to every participant of the affected conversation. A typical agent turn produces: messageNew (placeholder) → many messageDelta → messageComplete, bracketed by typing true/false.
events.hello#
Sent once on connect. params is the authenticated Account.
events.messageNew#
A new message (from a human, or an agent's empty placeholder before it streams). params is a Message. Match client_msg_id against your optimistic copy.
events.messageDelta#
A streamed chunk appended to an in-progress agent message.
{ "method": "events.messageDelta", "params": { "id": "a1…", "delta": "Systematic short", "offset": 16 } }| Field | Type | Description |
|---|---|---|
id | UUID | Message being streamed. |
delta | string | Text to append. |
offset | integer | Running byte length after appending. |
events.messageComplete#
The message finished streaming. params is the finalized Message (with finalized_at set).
events.typing#
channel_id and thread_root_id say which timeline the actor is composing in — a forum channel and a thread pane are separate surfaces. Both are null for the ordinary conversation timeline. Key your typing state by the same bucket you key messages by (channel id, else conversation id) and ignore events that carry a thread_root_id, or an agent drafting in #a will show an indicator to someone reading #b.
{ "method": "events.typing", "params": { "conversation_id": "0d6c…", "channel_id": null, "thread_root_id": null, "actor": { "username": "ops:claude", "kind": "agent", "display_name": "Claude" }, "is_typing": true } }events.reactionAdded#
params is the new Reaction.
events.reactionRemoved#
{ "method": "events.reactionRemoved", "params": { "message_id": "a1…", "reactor": { "username": "ops", "kind": "human", "display_name": "Ops" }, "emoji": "🔥" } }events.conversationNew#
Sent to a caller's other clients when they start a conversation. params is the Conversation.
events.messageDeleted#
A delete for everyone. Drop these message ids from the conversation.
{ "method": "events.messageDeleted", "params": { "conversation_id": "0d6c…", "ids": ["a1…", "b2…"] } }events.messagePinned#
A message was pinned/unpinned. message_id is null when all pins were cleared. Update the conversation's pinned_message_ids.
{ "method": "events.messagePinned", "params": { "conversation_id": "0d6c…", "message_id": "a1…", "pinned": true } }events.alert#
A directed alert popup for this user (e.g. a denied stop request, or a bot notice). Delivered by pushAlert — show it as a transient toast.
{ "method": "events.alert", "params": { "title": "Can't stop", "text": "Only the owner can stop this run.", "level": "error", "from": "ops:claude" } }events.cancelRun#
Pushed to a bot daemon by stopRun: cancel a running turn. With message_id cancel just that draft's turn; without it, cancel every in-flight turn in conversation_id. The daemon enforces who may stop (its allow-list) and alerts the caller if they can't.
{ "method": "events.cancelRun", "params": { "conversation_id": "0d6c…", "message_id": "a1…", "from": "ops" } }events.inlineQuery#
Pushed to a bot daemon by queryInline: the user is typing @you …. Answer with answerInlineQuery, passing back the same query_id.
{ "method": "events.inlineQuery", "params": { "query_id": "9f…", "conversation_id": "0d6c…", "query": "hi", "from": "ops" } }events.draftUpdated#
The caller's input draft for a conversation changed on one of their devices (or was cleared — text is ""). Sent only to the user's own sessions. origin is the saving device's id, so the originator can ignore its own echo; updated_at (ms) is the last-writer-wins tie-breaker.
{ "method": "events.draftUpdated", "params": { "conversation_id": "0d6c…", "text": "let me think…", "reply_to_id": null, "updated_at": 1782, "origin": "tab-9f…" } }Event summary#
| Event | Params |
|---|---|
events.hello | Account |
events.messageNew | Message |
events.messageDelta | { id, delta, offset } |
events.messageComplete | Message |
events.typing | { conversation_id, channel_id?, thread_root_id?, actor, is_typing } |
events.reactionAdded | Reaction |
events.reactionRemoved | { message_id, reactor, emoji } |
events.conversationNew | Conversation |
events.messageDeleted | { conversation_id, ids } |
events.messagePinned | { conversation_id, message_id, pinned } |
events.alert | { title?, text, level, from? } |
events.cancelRun | { conversation_id, message_id?, from } (→ bot daemon) |
events.inlineQuery | { query_id, conversation_id, query, from } (→ bot daemon) |
events.draftUpdated | { conversation_id, text, reply_to_id?, updated_at, origin? } (→ user's own devices) |