Skip to content

Build a client

An OriginChats client needs a WebSocket implementation, JSON parsing, a Rotur authentication flow, and a small event router.

Use the server’s public URL with a WebSocket scheme. The API is mounted at /.

const url = new URL('https://chat.example.com');
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
const socket = new WebSocket(url);

The first server frame is handshake. Save its val object; it is the source of truth for limits and feature availability.

The handshake includes a unique validator_key. Ask Rotur to sign or validate that challenge using your user’s session, then send the returned validator unchanged:

{
"cmd": "auth",
"validator": "<rotur validator>"
}

Success arrives as two ordered frames: auth_success, then ready. The ready.user value is the stored account object for the current user. Authentication failures use auth_error, not the general error envelope.

After authentication, tell the server which optional response shapes you understand:

{
"cmd": "capabilities",
"capabilities": [
"channel_get"
]
}

The legacy { "cmd": "capabilities", "val": [...] } form is also accepted. A client declaring channel_get can receive a focused channel_get refresh instead of the full channels_get list when one channel changes.

for (const cmd of ['channels_get', 'roles_list', 'users_online']) {
socket.send(JSON.stringify({ cmd, listener: crypto.randomUUID() }));
}

Treat listener as an opaque client correlation ID. The server copies it to every direct response produced by that request. Broadcast copies sent to other connections do not include it.

If the handshake advertises message_signatures_v1 or slash_signatures_v1, continue with signing and verification before sending authored content.

Route on cmd, not on object shape. Commands may produce a differently named event: poll_create produces message_new, and user_roles_set returns/broadcasts user_roles_get.

socket.addEventListener('message', ({ data }) => {
const event = JSON.parse(data);
if (event.cmd === 'error') rejectPending(event.listener, event.val, event.src);
else if (event.listener) resolvePending(event.listener, event);
else applyLiveEvent(event);
});

Respond to server ping frames if your runtime or networking stack expects application-level liveness. The server sends one about every 30 seconds.