WebSocket
WebSocket
One socket, any REST call as a frame, streamed events included. Useful in browsers and long-lived clients.
Connect and authenticate
Endpoint: ws://127.0.0.1:8765/ws. Browsers cannot set headers on a WebSocket, so the first frame authenticates:
- Send
{ "type": "auth", "token": "<token>" }; the node answers{ "type": "ready", "protocol": "clarkcant.ws.v1" }. - No auth within 10 seconds, or a wrong token: an
errorframe, then close code4401.
Frames
→ { "type": "auth", "token": "<token>" }
← { "type": "ready", "protocol": "clarkcant.ws.v1" }
→ { "type": "request", "id": "1", "method": "POST", "path": "/conversations/<conversationId>/messages/stream", "body": { "text": "hi" } }
← { "type": "event", "id": "1", "event": "delta", "data": { "text": "…" } }
← …
← { "type": "response", "id": "1", "status": 200, "body": null }
→ { "type": "ping" }
← { "type": "pong" }
| Frame | Direction | Shape |
|---|---|---|
auth | client → node | { type, token }, first frame only |
ready | node → client | { type, protocol: "clarkcant.ws.v1" } |
request | client → node | { type, id, method, path, query?, body? }: any REST route except a person's decision (403 PERSON_ONLY); id is echoed exactly as sent, string or number; query parameters go in query, not in path |
event | node → client | { type, id, event, data }: one per SSE event on streamed routes |
response | node → client | { type, id, status, body }: always last for its id; body is null after a stream |
ping / pong | both | { "type": "ping" } → { "type": "pong" } |
- Up to 16 requests may run concurrently per socket; give each a distinct
id. - A refused request (
INVALID_FRAME,TOO_MANY_REQUESTS) gets an{ type: "error", id, code, message }frame instead of aresponse; it is the last frame for thatid. - The event names are the same as the SSE stream:
delta,reasoning,tool-start,tool-end,host-control,error,done. - Binary routes (attachments, images) answer
415 USE_HTTP: use HTTP for bytes.
Example
const ws = new WebSocket("ws://127.0.0.1:8765/ws");
// Browsers cannot set headers on a WebSocket, so the first frame authenticates.
ws.onopen = () => ws.send(JSON.stringify({ type: "auth", token: "<token>" }));
ws.onmessage = (message) => {
const frame = JSON.parse(message.data);
if (frame.type === "ready") {
ws.send(JSON.stringify({
type: "request",
id: "1",
method: "POST",
path: "/conversations/<conversationId>/messages/stream",
body: { text: "hi" },
}));
}
if (frame.type === "event" && frame.event === "delta") console.log(frame.data.text);
if (frame.type === "response") console.log("finished", frame.id, frame.status);
};
ws.onclose = (event) => { if (event.code === 4401) console.log("token missing, wrong or too late"); };
Do not ship the token in a public page. Whoever holds it can drive your Clark. Use this from your own machine or behind your own login.
Other sockets
The node also serves /voice (a voice session) and /terminal (the Terminal card). These are app sockets, not public integration surfaces or part of this frame protocol.