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.
400">import { io } 400">from 400">class="text-emerald-400">'socket.io-client';
400">const socket = io(400">class="text-emerald-400">'wss:400">class="text-zinc-500">//api.lvng.ai', {
auth: {
token: 400">class="text-emerald-400">'YOUR_JWT_TOKEN'
},
transports: [400">class="text-emerald-400">'websocket', 400">class="text-emerald-400">'polling']
});
400">class="text-zinc-500">// Connection established
socket.on(400">class="text-emerald-400">'connected', (data) => {
console.log(400">class="text-emerald-400">'Connected:', data);
400">class="text-zinc-500">// { socketId, userId, organizationId, timestamp }
});
400">class="text-zinc-500">// Handle errors
socket.on(400">class="text-emerald-400">'error', (err) => {
console.error(400">class="text-emerald-400">'Socket error:', err);
400">class="text-zinc-500">// { code, message }
});
socket.on(400">class="text-emerald-400">'disconnect', (reason) => {
console.log(400">class="text-emerald-400">'Disconnected:', reason);
});| Parameter | Value |
|---|---|
| Server | wss://api.lvng.ai |
| Auth | JWT token passed in the auth.token handshake field |
| Transports | WebSocket (preferred), HTTP long-polling (fallback) |
| Ping interval | 25000 ms (25s) |
| Ping timeout | 60000 ms (60s) |
| Max buffer size | 1e6 (1 MB) |
| Rate limit | 100 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 Pattern | Scope |
|---|---|
org:{organizationId} | Organization-wide events (presence 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.
400">class="text-zinc-500">// Send a message to a channel
socket.emit(400">class="text-emerald-400">'message:send', {
channelId: 400">class="text-emerald-400">'550e8400-e29b-41d4-a716-446655440000',
content: 400">class="text-emerald-400">'Hello 400">from WebSocket!',
optimisticId: 400">class="text-emerald-400">'tmp_abc123', 400">class="text-zinc-500">// Optional, for optimistic UI
threadId: null, 400">class="text-zinc-500">// Optional, for threaded replies
mentions: [400">class="text-emerald-400">'usr_8f3a2b1c-4d5e-...'], 400">class="text-zinc-500">// Optional
workspaceId: 400">class="text-emerald-400">'ws_b2c3d4e5-...' 400">class="text-zinc-500">// Optional
});
400">class="text-zinc-500">// Confirmation sent back to sender only
socket.on(400">class="text-emerald-400">'message:sent', (ack) => {
console.log(400">class="text-emerald-400">'Confirmed:', ack);
400">class="text-zinc-500">// { id, optimisticId, channelId, timestamp }
});
400">class="text-zinc-500">// New message broadcast to the room
socket.on(400">class="text-emerald-400">'message:400">new', (message) => {
console.log(400">class="text-emerald-400">'New message:', message);
400">class="text-zinc-500">// { id, channelId, conversationId, userId, userName,
400">class="text-zinc-500">// content, contentType, threadId, attachments,
400">class="text-zinc-500">// createdAt, updatedAt, metadata }
});
400">class="text-zinc-500">// Message edited
socket.on(400">class="text-emerald-400">'message:update', (update) => {
console.log(400">class="text-emerald-400">'Updated:', update);
400">class="text-zinc-500">// { messageId, content, metadata, channelId,
400">class="text-zinc-500">// conversationId, updatedBy, timestamp }
});
400">class="text-zinc-500">// Message deleted
socket.on(400">class="text-emerald-400">'message:delete', (data) => {
console.log(400">class="text-emerald-400">'Deleted:', data);
400">class="text-zinc-500">// { messageId, channelId, conversationId,
400">class="text-zinc-500">// deletedBy, deletedByName, timestamp }
});
400">class="text-zinc-500">// Reaction added or removed
socket.on(400">class="text-emerald-400">'message:reaction', (reaction) => {
console.log(400">class="text-emerald-400">'Reaction:', reaction);
400">class="text-zinc-500">// { messageId, channelId, emoji, userId,
400">class="text-zinc-500">// userName, action: 400">class="text-emerald-400">'add'|400">class="text-emerald-400">'remove', timestamp }
});
400">class="text-zinc-500">// Read receipt
socket.on(400">class="text-emerald-400">'message:read', (receipt) => {
console.log(400">class="text-emerald-400">'Read:', receipt);
400">class="text-zinc-500">// { messageId, channelId, conversationId,
400">class="text-zinc-500">// userId, userName, timestamp }
});
400">class="text-zinc-500">// Batch read receipts
socket.on(400">class="text-emerald-400">'message:read:batch', (batch) => {
console.log(400">class="text-emerald-400">'Batch read:', batch);
400">class="text-zinc-500">// { messageIds: [], channelId, conversationId,
400">class="text-zinc-500">// userId, timestamp }
});Message Events
| Event | Direction | Payload |
|---|---|---|
message:send | Client → Server | {channelId, conversationId?, customerId?, content, threadId?, mentions?, optimisticId?, workspaceId?} |
message:new | Server → Client | {id, channelId, conversationId, userId, userName, content, contentType, threadId, attachments, createdAt, updatedAt, metadata} |
message:sent | Server → Client | {id, optimisticId, channelId, timestamp} |
message:update | Client → Server | {messageId, channelId, conversationId?, customerId?, content, metadata?} |
message:update | Server → Client | {messageId, content, metadata, channelId, conversationId, updatedBy, timestamp} |
message:delete | Client → Server | {messageId, channelId, conversationId?, customerId?} |
message:delete | Server → Client | {messageId, channelId, conversationId, deletedBy, deletedByName, timestamp} |
message:react | Client → Server | {messageId, channelId, emoji, action: "add"|"remove"} |
message:reaction | Server → Client | {messageId, channelId, emoji, userId, userName, action, timestamp} |
message:read | Client → Server | {messageId, channelId, conversationId?, customerId?} |
message:read | Server → Client | {messageId, channelId, conversationId, userId, userName, timestamp} |
message:read:batch | Server → Client | {messageIds: [], channelId, conversationId, userId, timestamp} |
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.
400">class="text-zinc-500">// Start typing indicator
socket.emit(400">class="text-emerald-400">'typing:start', {
channelId: 400">class="text-emerald-400">'550e8400-e29b-41d4-a716-446655440000',
userId: 400">class="text-emerald-400">'usr_8f3a2b1c-...',
userName: 400">class="text-emerald-400">'Matty'
});
400">class="text-zinc-500">// Stop typing indicator
socket.emit(400">class="text-emerald-400">'typing:stop', {
channelId: 400">class="text-emerald-400">'550e8400-e29b-41d4-a716-446655440000',
userId: 400">class="text-emerald-400">'usr_8f3a2b1c-...',
userName: 400">class="text-emerald-400">'Matty'
});
400">class="text-zinc-500">// Listen for others typing
socket.on(400">class="text-emerald-400">'typing:start', (data) => {
console.log(data.userName, 400">class="text-emerald-400">'is typing in', data.channelId);
400">class="text-zinc-500">// { channelId, conversationId, userId, userName, timestamp }
});
socket.on(400">class="text-emerald-400">'typing:stop', (data) => {
console.log(data.userName, 400">class="text-emerald-400">'stopped typing');
400">class="text-zinc-500">// { channelId, conversationId, userId, userName, timestamp }
});Typing Events
| Event | Direction | Payload |
|---|---|---|
typing:start | Client → Server | {channelId, conversationId?, customerId?, userId, userName} |
typing:start | Server → Client | {channelId, conversationId, userId, userName, timestamp} |
typing:stop | Client → Server | {channelId, conversationId?, customerId?, userId, userName} |
typing:stop | Server → 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.
400">class="text-zinc-500">// Join a channel room to receive its events
socket.emit(400">class="text-emerald-400">'channel:join', { channelId: 400">class="text-emerald-400">'550e8400-...' }, (response) => {
console.log(400">class="text-emerald-400">'Joined channel:', response);
});
400">class="text-zinc-500">// Server broadcasts join to other members
400">class="text-zinc-500">// Other clients receive:
400">class="text-zinc-500">// { channelId, userId, userName, user: { id, name }, timestamp }
400">class="text-zinc-500">// Channel history is sent to the joining client
socket.on(400">class="text-emerald-400">'channel:history', (data) => {
console.log(400">class="text-emerald-400">'History:', data);
400">class="text-zinc-500">// { channelId, messages: [...], hasMore, total }
400">class="text-zinc-500">// Last 50 messages by 400">default
});
400">class="text-zinc-500">// Leave a channel room
socket.emit(400">class="text-emerald-400">'channel:leave', { channelId: 400">class="text-emerald-400">'550e8400-...' }, (response) => {
console.log(400">class="text-emerald-400">'Left channel:', response);
});
400">class="text-zinc-500">// Listen for channel metadata updates
socket.on(400">class="text-emerald-400">'channel:update', (data) => {
console.log(400">class="text-emerald-400">'Channel updated:', data);
400">class="text-zinc-500">// { channelId, updates, channel, updatedBy, updatedByName, timestamp }
});Channel Events
| Event | Direction | Payload |
|---|---|---|
channel:join | Client → Server | {channelId} with callback |
channel:join | Server → Client | {channelId, userId, userName, user: {id, name}, timestamp} |
channel:history | Server → Client | {channelId, messages: [], hasMore, total} |
channel:leave | Client → Server | {channelId} with callback |
channel:leave | Server → Client | {channelId, userId, userName, timestamp} |
channel:update | Client → Server | {channelId, updates: {...}} |
channel:update | Server → 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.
400">class="text-zinc-500">// Update your presence status
socket.emit(400">class="text-emerald-400">'presence:update', {
status: 400">class="text-emerald-400">'online', 400">class="text-zinc-500">// 400">class="text-emerald-400">'online' | 400">class="text-emerald-400">'away' | 400">class="text-emerald-400">'busy' | 400">class="text-emerald-400">'offline'
customStatus: 400">class="text-emerald-400">'In a meeting',
statusText: 400">class="text-emerald-400">'Back at 3pm'
});
400">class="text-zinc-500">// Keep your status alive (send periodically)
socket.emit(400">class="text-emerald-400">'presence:heartbeat');
400">class="text-zinc-500">// Listen for presence changes 400">from your organization
socket.on(400">class="text-emerald-400">'presence:update', (data) => {
console.log(data.userName, 400">class="text-emerald-400">'is now', data.status);
400">class="text-zinc-500">// { userId, userName, status, customStatus, timestamp }
});Presence Events
| Event | Direction | Payload |
|---|---|---|
presence:update | Client → Server | {status: "online"|"away"|"busy"|"offline", customStatus?, statusText?} |
presence:update | Server → Client | {userId, userName, status, customStatus, timestamp} |
presence:heartbeat | Client → 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.
400">class="text-zinc-500">// Join a multi-tenant conversation
socket.emit(400">class="text-emerald-400">'conversation:join', {
conversationId: 400">class="text-emerald-400">'conv_a1b2c3d4-...',
customerId: 400">class="text-emerald-400">'cust_e5f6a7b8-...'
});
400">class="text-zinc-500">// Confirmation sent to sender
socket.on(400">class="text-emerald-400">'room:joined', (data) => {
console.log(400">class="text-emerald-400">'Joined room:', data);
400">class="text-zinc-500">// { room, conversationId, customerId, timestamp }
});
400">class="text-zinc-500">// Other members in the room see:
400">class="text-zinc-500">// member:joined -> { conversationId, userId, userName, timestamp }
400">class="text-zinc-500">// Leave a conversation
socket.emit(400">class="text-emerald-400">'conversation:leave', {
conversationId: 400">class="text-emerald-400">'conv_a1b2c3d4-...',
customerId: 400">class="text-emerald-400">'cust_e5f6a7b8-...'
});
400">class="text-zinc-500">// Rejoin after disconnect (get missed messages)
socket.emit(400">class="text-emerald-400">'conversation:rejoin', {
conversationId: 400">class="text-emerald-400">'conv_a1b2c3d4-...',
customerId: 400">class="text-emerald-400">'cust_e5f6a7b8-...',
lastMessageId: 400">class="text-emerald-400">'msg_f7e6d5c4-...', 400">class="text-zinc-500">// Optional
lastMessageTimestamp: 400">class="text-emerald-400">'2026-03-19T12:00Z' 400">class="text-zinc-500">// Optional
});
socket.on(400">class="text-emerald-400">'conversation:rejoined', (data) => {
console.log(400">class="text-emerald-400">'Missed messages:', data.missed_messages);
400">class="text-zinc-500">// { conversationId, customerId, missed_messages: [], timestamp }
});Conversation Events
| Event | Direction | Payload |
|---|---|---|
conversation:join | Client → Server | {conversationId, customerId} |
room:joined | Server → Client | {room, conversationId, customerId, timestamp} |
member:joined | Server → Client | {conversationId, userId, userName, timestamp} |
conversation:leave | Client → Server | {conversationId, customerId} |
member:left | Server → Client | {conversationId, userId, userName, timestamp} |
conversation:rejoin | Client → Server | {conversationId, customerId, lastMessageId?, lastMessageTimestamp?} |
conversation:rejoined | Server → Client | {conversationId, customerId, missed_messages: [], timestamp} |
Calls
Call rooms use the call:{roomName} pattern. Participants can send live transcription segments which are broadcast to all other participants in the call.
400">class="text-zinc-500">// Join a call room
socket.emit(400">class="text-emerald-400">'call:join', { roomName: 400">class="text-emerald-400">'call_engineering_standup' });
socket.on(400">class="text-emerald-400">'call:joined', (data) => {
console.log(400">class="text-emerald-400">'Joined call:', data);
400">class="text-zinc-500">// { roomName, timestamp }
});
400">class="text-zinc-500">// Others in the call receive:
400">class="text-zinc-500">// call:participant_joined -> { roomName, userId, userName, timestamp }
400">class="text-zinc-500">// Send live transcription segments
socket.emit(400">class="text-emerald-400">'call:transcription', {
roomName: 400">class="text-emerald-400">'call_engineering_standup',
segment: {
speaker: 400">class="text-emerald-400">'Matty',
text: 400">class="text-emerald-400">'Let us review the sprint backlog.',
isFinal: true,
confidence: 0.97
}
});
400">class="text-zinc-500">// Receive transcription segments 400">from others
socket.on(400">class="text-emerald-400">'transcription:segment', (segment) => {
console.log(segment.speaker + 400">class="text-emerald-400">':', segment.text);
400">class="text-zinc-500">// { speaker, text, isFinal, timestamp, confidence }
});
400">class="text-zinc-500">// Leave the call
socket.emit(400">class="text-emerald-400">'call:leave', { roomName: 400">class="text-emerald-400">'call_engineering_standup' });
400">class="text-zinc-500">// Others receive:
400">class="text-zinc-500">// call:participant_left -> { roomName, userId, userName, reason, timestamp }Call Events
| Event | Direction | Payload |
|---|---|---|
call:join | Client → Server | {roomName} |
call:joined | Server → Client | {roomName, timestamp} |
call:participant_joined | Server → Client | {roomName, userId, userName, timestamp} |
call:leave | Client → Server | {roomName} |
call:participant_left | Server → Client | {roomName, userId, userName, reason, timestamp} |
call:transcription | Client → Server | {roomName, segment} |
transcription:segment | Server → Client | {speaker, text, isFinal, timestamp, confidence} |
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.
400">class="text-zinc-500">// Subscribe to a swarm session's updates
socket.emit(400">class="text-emerald-400">'swarm:subscribe', {
sessionId: 400">class="text-emerald-400">'swarm_9a8b7c6d-...'
});
socket.on(400">class="text-emerald-400">'swarm:subscribed', (data) => {
console.log(400">class="text-emerald-400">'Subscribed to swarm:', data.sessionId);
400">class="text-zinc-500">// { sessionId }
});
400">class="text-zinc-500">// Unsubscribe when done
socket.emit(400">class="text-emerald-400">'swarm:unsubscribe', {
sessionId: 400">class="text-emerald-400">'swarm_9a8b7c6d-...'
});Swarm Events
| Event | Direction | Payload |
|---|---|---|
swarm:subscribe | Client → Server | {sessionId} |
swarm:subscribed | Server → Client | {sessionId} |
swarm:unsubscribe | Client → Server | {sessionId} |
Connection Events
These events are emitted by the server immediately after a successful or failed connection.
Connection Events
| Event | Direction | Payload |
|---|---|---|
connected | Server → Client | {socketId, userId, organizationId, timestamp} |
error | Server → 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.