WebSocket Events

LVNG uses Socket.io for real-time communication. Connect to receive live messages, typing indicators, presence updates, conversation events, call transcription, and swarm session updates.

Connection

Connect to the Socket.io server with your JWT token for authentication. The server supports both WebSocket (preferred) and HTTP long-polling as a fallback.

connection.ts
import { io } from 'socket.io-client';

const socket = io('wss://api.lvng.ai', {
  auth: {
    token: 'YOUR_JWT_TOKEN'
  },
  transports: ['websocket', 'polling']
});

// Connection established
socket.on('connected', (data) => {
  console.log('Connected:', data);
  // { socketId, userId, organizationId, timestamp }
});

// Handle errors
socket.on('error', (err) => {
  console.error('Socket error:', err);
  // { code, message }
});

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});
ParameterValue
Serverwss://api.lvng.ai
AuthJWT token passed in the auth.token handshake field
TransportsWebSocket (preferred), HTTP long-polling (fallback)
Ping interval25000 ms (25s)
Ping timeout60000 ms (60s)
Max buffer size1e6 (1 MB)
Rate limit100 events per 60 seconds per socket

Note: You must join a channel with channel:join or a conversation with conversation:join before receiving events for that room.

Room Structure

Events are scoped to rooms. The server automatically manages room membership based on your join/leave actions. Each room type uses a specific naming convention.

Room PatternScope
org:{organizationId}Organization-wide events (presence updates)
user:{userId}Personal room for user-targeted events (call notifications, OODA updates)
channel:{channelId}Channel messages, typing, and member events
customer:{customerId}:conversation:{conversationId}Multi-tenant DMs and group conversations
call:{roomName}Call participants and live transcription
swarm:{sessionId}Agent swarm session subscribers
activity:{customerId}Activity feed for a customer

Messages

Send and receive messages in real-time. When you emit message:send, the server validates the payload, persists the message, and broadcasts message:new to the room. The sender also receives a message:sent acknowledgment with the server-assigned ID mapped to your optimisticId.

messages.ts
// Send a message to a channel
socket.emit('message:send', {
  channelId: '550e8400-e29b-41d4-a716-446655440000',
  content: 'Hello from WebSocket!',
  optimisticId: 'tmp_abc123',          // Optional, for optimistic UI
  threadId: null,                       // Optional, for threaded replies
  mentions: ['usr_8f3a2b1c-4d5e-...'], // Optional
  workspaceId: 'ws_b2c3d4e5-...'       // Optional
});

// Confirmation sent back to sender only
socket.on('message:sent', (ack) => {
  console.log('Confirmed:', ack);
  // { id, optimisticId, channelId, timestamp }
});

// New message broadcast to the room
socket.on('message:new', (message) => {
  console.log('New message:', message);
  // { id, channelId, conversationId, userId, userName,
  //   content, contentType, threadId, attachments,
  //   createdAt, updatedAt, metadata }
});

// Message edited
socket.on('message:update', (update) => {
  console.log('Updated:', update);
  // { messageId, content, metadata, channelId,
  //   conversationId, updatedBy, timestamp }
});

// Message deleted
socket.on('message:delete', (data) => {
  console.log('Deleted:', data);
  // { messageId, channelId, conversationId,
  //   deletedBy, deletedByName, timestamp }
});

// Reaction added or removed
socket.on('message:reaction', (reaction) => {
  console.log('Reaction:', reaction);
  // { messageId, channelId, emoji, userId,
  //   userName, action: 'add'|'remove', timestamp }
});

// Read receipt
socket.on('message:read', (receipt) => {
  console.log('Read:', receipt);
  // { messageId, channelId, conversationId,
  //   userId, userName, timestamp }
});

// Batch read receipts
socket.on('message:read:batch', (batch) => {
  console.log('Batch read:', batch);
  // { messageIds: [], channelId, conversationId,
  //   userId, timestamp }
});

Message Events

EventDirectionPayload
message:sendClient → Server{channelId, conversationId?, customerId?, content, threadId?, mentions?, optimisticId?, workspaceId?}
message:newServer → Client{id, channelId, conversationId, userId, userName, content, contentType, threadId, attachments, createdAt, updatedAt, metadata}
message:sentServer → Client{id, optimisticId, channelId, timestamp}
message:updateClient → Server{messageId, channelId, conversationId?, customerId?, content, metadata?}
message:updateServer → Client{messageId, content, metadata, channelId, conversationId, updatedBy, timestamp}
message:deleteClient → Server{messageId, channelId, conversationId?, customerId?}
message:deleteServer → Client{messageId, channelId, conversationId, deletedBy, deletedByName, timestamp}
message:reactClient → Server{messageId, channelId, emoji, action: "add"|"remove"}
message:reactionServer → Client{messageId, channelId, emoji, userId, userName, action, timestamp}
message:readClient → Server{messageId, channelId, conversationId?, customerId?}
message:readServer → Client{messageId, channelId, conversationId, userId, userName, timestamp}
message:read:batchServer → Client{messageIds: [], channelId, conversationId, userId, timestamp}
message:send_queuedClient → Server{messages: [{channelId, content, ...}]}

Typing Indicators

Emit typing:start when the user begins typing and typing:stop when they pause or clear the input. The server rebroadcasts the event to all other members of the channel or conversation room.

typing.ts
// Start typing indicator
socket.emit('typing:start', {
  channelId: '550e8400-e29b-41d4-a716-446655440000',
  userId: 'usr_8f3a2b1c-...',
  userName: 'Matty'
});

// Stop typing indicator
socket.emit('typing:stop', {
  channelId: '550e8400-e29b-41d4-a716-446655440000',
  userId: 'usr_8f3a2b1c-...',
  userName: 'Matty'
});

// Listen for others typing
socket.on('typing:start', (data) => {
  console.log(data.userName, 'is typing in', data.channelId);
  // { channelId, conversationId, userId, userName, timestamp }
});

socket.on('typing:stop', (data) => {
  console.log(data.userName, 'stopped typing');
  // { channelId, conversationId, userId, userName, timestamp }
});

Typing Events

EventDirectionPayload
typing:startClient → Server{channelId, conversationId?, customerId?, userId, userName}
typing:startServer → Client{channelId, conversationId, userId, userName, timestamp}
typing:stopClient → Server{channelId, conversationId?, customerId?, userId, userName}
typing:stopServer → Client{channelId, conversationId, userId, userName, timestamp}

Channels

Join a channel room to receive its messages, typing indicators, and update events. On join, the server sends the last 50 messages as channel:history. Both channel:join and channel:leave support Socket.io callback acknowledgments.

channels.ts
// Join a channel room to receive its events
socket.emit('channel:join', { channelId: '550e8400-...' }, (response) => {
  console.log('Joined channel:', response);
});

// Server broadcasts join to other members
// Other clients receive:
// { channelId, userId, userName, user: { id, name }, timestamp }

// Channel history is sent to the joining client
socket.on('channel:history', (data) => {
  console.log('History:', data);
  // { channelId, messages: [...], hasMore, total }
  // Last 50 messages by default
});

// Leave a channel room
socket.emit('channel:leave', { channelId: '550e8400-...' }, (response) => {
  console.log('Left channel:', response);
});

// Listen for channel metadata updates
socket.on('channel:update', (data) => {
  console.log('Channel updated:', data);
  // { channelId, updates, channel, updatedBy, updatedByName, timestamp }
});

Channel Events

EventDirectionPayload
channel:joinClient → Server{channelId} with callback
channel:joinServer → Client{channelId, userId, userName, user: {id, name}, timestamp}
channel:historyServer → Client{channelId, messages: [], hasMore, total}
channel:leaveClient → Server{channelId} with callback
channel:leaveServer → Client{channelId, userId, userName, timestamp}
channel:updateClient → Server{channelId, updates: {...}}
channel:updateServer → Client{channelId, updates, channel, updatedBy, updatedByName, timestamp}

Presence

Presence events are broadcast to the org:{organizationId} room, so all members of the same organization see status changes. Send periodic heartbeats to keep your status from timing out.

presence.ts
// Update your presence status
socket.emit('presence:update', {
  status: 'online',           // 'online' | 'away' | 'busy' | 'offline'
  customStatus: 'In a meeting',
  statusText: 'Back at 3pm'
});

// Keep your status alive (send periodically)
socket.emit('presence:heartbeat');

// Listen for presence changes from your organization
socket.on('presence:update', (data) => {
  console.log(data.userName, 'is now', data.status);
  // { userId, userName, status, customStatus, timestamp }
});

Presence Events

EventDirectionPayload
presence:updateClient → Server{status: "online"|"away"|"busy"|"offline", customStatus?, statusText?}
presence:updateServer → Client{userId, userName, status, customStatus, timestamp}
presence:heartbeatClient → Server(empty)

Conversations

Multi-tenant conversations use a customer:{customerId}:conversation:{conversationId} room pattern. The conversation:rejoin event lets you reconnect after a disconnect and retrieve any missed messages since your last known message.

conversations.ts
// Join a multi-tenant conversation
socket.emit('conversation:join', {
  conversationId: 'conv_a1b2c3d4-...',
  customerId: 'cust_e5f6a7b8-...'
});

// Confirmation sent to sender
socket.on('room:joined', (data) => {
  console.log('Joined room:', data);
  // { room, conversationId, customerId, timestamp }
});

// Other members in the room see:
// member:joined -> { conversationId, userId, userName, timestamp }

// Leave a conversation
socket.emit('conversation:leave', {
  conversationId: 'conv_a1b2c3d4-...',
  customerId: 'cust_e5f6a7b8-...'
});

// Rejoin after disconnect (get missed messages)
socket.emit('conversation:rejoin', {
  conversationId: 'conv_a1b2c3d4-...',
  customerId: 'cust_e5f6a7b8-...',
  lastMessageId: 'msg_f7e6d5c4-...',       // Optional
  lastMessageTimestamp: '2026-03-19T12:00Z' // Optional
});

socket.on('conversation:rejoined', (data) => {
  console.log('Missed messages:', data.missed_messages);
  // { conversationId, customerId, missed_messages: [], timestamp }
});

Conversation Events

EventDirectionPayload
conversation:joinClient → Server{conversationId, customerId}
room:joinedServer → Client{room, conversationId, customerId, timestamp}
member:joinedServer → Client{conversationId, userId, userName, timestamp}
conversation:leaveClient → Server{conversationId, customerId}
member:leftServer → Client{conversationId, userId, userName, timestamp}
conversation:rejoinClient → Server{conversationId, customerId, lastMessageId?, lastMessageTimestamp?}
conversation:rejoinedServer → Client{conversationId, customerId, missed_messages: [], timestamp}
conversation:mark_readClient → Server{conversationId, customerId}

Calls

Call rooms use the call:{roomName} pattern. Participants can send live transcription segments which are broadcast to all other participants in the call.

calls.ts
// Join a call room
socket.emit('call:join', { roomName: 'call_engineering_standup' });

socket.on('call:joined', (data) => {
  console.log('Joined call:', data);
  // { roomName, timestamp }
});

// Others in the call receive:
// call:participant_joined -> { roomName, userId, userName, timestamp }

// Send live transcription segments
socket.emit('call:transcription', {
  roomName: 'call_engineering_standup',
  segment: {
    speaker: 'Matty',
    text: 'Let us review the sprint backlog.',
    isFinal: true,
    confidence: 0.97
  }
});

// Receive transcription segments from others
socket.on('transcription:segment', (segment) => {
  console.log(segment.speaker + ':', segment.text);
  // { speaker, text, isFinal, timestamp, confidence }
});

// Leave the call
socket.emit('call:leave', { roomName: 'call_engineering_standup' });

// Others receive:
// call:participant_left -> { roomName, userId, userName, reason, timestamp }

Call Events

EventDirectionPayload
call:joinClient → Server{roomName}
call:joinedServer → Client{roomName, timestamp}
call:participant_joinedServer → Client{roomName, userId, userName, timestamp}
call:leaveClient → Server{roomName}
call:participant_leftServer → Client{roomName, userId, userName, reason, timestamp}
call:declineClient → Server{roomId, callerId}
call:declinedServer → Client{roomId, declinedBy, declinedByName, timestamp}
call:cancelClient → Server{roomId, recipientId?, roomName?}
call:cancelledServer → Client{roomId, roomName?, cancelledBy, cancelledByName, timestamp}
call:transcriptionClient → Server{roomName, segment}
call:audio-chunkClient → Server{roomName, chunk}
transcription:segmentServer → Client{speaker, text, isFinal, timestamp, confidence}
transcription:resultServer → Client{roomName, text, isFinal, confidence, timestamp}

Swarm Sessions

Subscribe to a swarm session to receive real-time updates as agents in the swarm complete steps. The server joins you to the swarm:{sessionId} room.

swarm.ts
// Subscribe to a swarm session's updates
socket.emit('swarm:subscribe', {
  sessionId: 'swarm_9a8b7c6d-...'
});

socket.on('swarm:subscribed', (data) => {
  console.log('Subscribed to swarm:', data.sessionId);
  // { sessionId }
});

// Unsubscribe when done
socket.emit('swarm:unsubscribe', {
  sessionId: 'swarm_9a8b7c6d-...'
});

Swarm Events

EventDirectionPayload
swarm:subscribeClient → Server{sessionId}
swarm:subscribedServer → Client{sessionId}
swarm:unsubscribeClient → Server{sessionId}

Activity Feed

Join a tenant-isolated activity feed room to receive real-time activity events for your workspace. The room format is activity:{customerId}.

activity.ts
// Join an activity feed room for your workspace
socket.emit('activity:join', {
  room: 'activity:cust_e5f6a7b8-...'
});

socket.on('activity:joined', (data) => {
  console.log('Joined activity feed:', data.room);
  // { room }
});

// Leave the activity feed
socket.emit('activity:leave', {
  room: 'activity:cust_e5f6a7b8-...'
});

Activity Events

EventDirectionPayload
activity:joinClient → Server{room: "activity:{customerId}"}
activity:joinedServer → Client{room}
activity:leaveClient → Server{room}

OODA Loop

When an OODA loop agent executes via the streaming API, phase updates are emitted to the user's personal user:{userId} room. Each phase (observe, orient, decide, act) emits start and completion events.

ooda.ts
// OODA loop phase events are emitted during streaming execution
// Subscribe by joining the user room (automatic on connection)

socket.on('ooda:phase', (data) => {
  console.log('OODA phase:', data.phase, data.data.status);
  // { phase: 'observe'|'orient'|'decide'|'act',
  //   iteration: 0, data: { status: 'started'|'completed', result? },
  //   timestamp }
});

socket.on('ooda:complete', (data) => {
  console.log('OODA complete:', data);
  // { goalMet: boolean, iterations: number,
  //   killed: boolean, timestamp }
});

OODA Events

EventDirectionPayload
ooda:phaseServer → Client{phase: "observe"|"orient"|"decide"|"act", iteration, data: {status, result?}, timestamp}
ooda:completeServer → Client{goalMet, iterations, killed, timestamp}

Tool Permissions

The agentic harness can request user approval before executing sensitive tools. When a tool requires permission, the server emits a request event and waits up to 30 seconds for a response.

permissions.ts
// Tool permission events for agentic harness approval flows
socket.on('tool:permission:request', (data) => {
  console.log('Tool needs approval:', data.toolName);
  // { requestId, toolName, input, reason }

  // Respond with approval or denial
  socket.emit('tool:permission:response', {
    requestId: data.requestId,
    approved: true
  });
});

Tool Permission Events

EventDirectionPayload
tool:permission:requestServer → Client{requestId, toolName, input, reason}
tool:permission:responseClient → Server{requestId, approved: boolean, reason?}

Connection Events

These events are emitted by the server immediately after a successful or failed connection.

Connection Events

EventDirectionPayload
connectedServer → Client{socketId, userId, organizationId, timestamp}
errorServer → Client{code, message}

Reconnection: Socket.io handles automatic reconnection with exponential backoff. After reconnecting, re-join your channels and conversations, and use conversation:rejoin to fetch missed messages.