Skip to main content

Real Time Block

Real Time Block

Complete real-time communication for any application

The Real Time Block provides everything you need for instant, bidirectional communication. Build chat applications, video conferencing, live collaboration, and real-time dashboards with WebSocket connections, WebRTC support, presence tracking, and message history.

Features

FeatureDescription
WebSocket ConnectionsPersistent bidirectional connections with auto-reconnect
Channels & RoomsOrganize communication with public, private, and presence channels
Presence TrackingKnow who is online with custom status states
WebRTC SupportPeer-to-peer video, audio, and screen sharing
Message HistoryPersist and retrieve past messages with pagination
Broadcast EventsSend messages to thousands of subscribers instantly
Typing IndicatorsShow real-time typing status to other users
Read ReceiptsTrack message delivery and read status
AI Conversation SummariesLLM-powered structured summaries with categories, action items, and stats
AI Cross-Conversation DigestAggregate all unread conversations into a single AI-generated digest
Smart Unread TrackingPer-conversation unread counts with role-aware notifications and filtering

API Endpoint

ServiceURL
Real Timehttps://conversations.api.us.23blocks.com

Environment Routing: Use the same URL for all environments. Your API key determines the environment:

  • pk_test_* / sk_test_* → Routes to Staging
  • pk_live_* / sk_live_* → Routes to Production

Quick Start

Installation

npm install @23blocks/sdk

Initialize the Client

import { create23BlocksClient } from '@23blocks/sdk';

const client = create23BlocksClient({
urls: { realtime: 'https://conversations.api.us.23blocks.com' },
apiKey: 'your-api-key', // Use pk_test_* for staging, pk_live_* for production
});

Basic Usage

// Connect to real-time service
const realtime = await client.realtime.connect({
token: 'user-auth-token'
});

// Subscribe to a channel
const channel = realtime.subscribe('chat:room-1');

// Listen for messages
channel.on('message', (message) => {
console.log('New message:', message);
});

// Listen for presence changes
channel.on('presence', (users) => {
console.log('Online users:', users);
});

// Send a message
channel.send({
type: 'message',
text: 'Hello, everyone!',
user_id: 'user_123'
});

// Track typing status
channel.typing.start();
// ... after user stops typing
channel.typing.stop();

API Reference

Base URL

wss://realtime.23blocks.com
https://api.23blocks.com/v1/realtime

Authentication

WebSocket connections require a connection token:

// Get connection token
const { token } = await client.realtime.getToken({
user_id: 'user_123',
channels: ['chat:*', 'notifications:user_123']
});

// Connect with token
const realtime = await client.realtime.connect({ token });

REST API calls require standard headers:

X-App-Id: your-app-id
X-Api-Key: your-api-key
Authorization: Bearer <user-token>

Connection API

Manage WebSocket connections with automatic reconnection and state handling.

Connect

Establish a WebSocket connection.

const realtime = await client.realtime.connect({
token: 'connection-token',
options: {
autoReconnect: true,
reconnectDelay: 1000,
maxReconnectAttempts: 10,
heartbeatInterval: 30000
}
});

Connection Events:

realtime.on('connected', () => {
console.log('Connected to real-time service');
});

realtime.on('disconnected', (reason) => {
console.log('Disconnected:', reason);
});

realtime.on('reconnecting', (attempt) => {
console.log('Reconnecting, attempt:', attempt);
});

realtime.on('error', (error) => {
console.error('Connection error:', error);
});

Disconnect

Close the WebSocket connection.

realtime.disconnect();

Connection State

Check connection status.

const state = realtime.getState();
// 'connecting' | 'connected' | 'disconnecting' | 'disconnected'

const isConnected = realtime.isConnected();

Channels API

Organize communication with channels. Channels can be public, private, or presence-enabled.

Channel Types

TypePrefixDescription
Publicpublic:Anyone can subscribe and send messages
Privateprivate:Requires authorization to subscribe
Presencepresence:Includes presence tracking for subscribers

Subscribe to Channel

// Subscribe to a public channel
const channel = realtime.subscribe('public:news');

// Subscribe to a private channel
const privateChannel = realtime.subscribe('private:team-123');

// Subscribe to a presence channel
const presenceChannel = realtime.subscribe('presence:room-1');

Channel Events

// Message received
channel.on('message', (message) => {
console.log('Message:', message);
});

// Custom event
channel.on('custom-event', (data) => {
console.log('Custom event:', data);
});

// Presence updates (presence channels only)
channel.on('presence:join', (user) => {
console.log('User joined:', user);
});

channel.on('presence:leave', (user) => {
console.log('User left:', user);
});

channel.on('presence:update', (user) => {
console.log('User updated:', user);
});

Send Message

channel.send({
type: 'message',
data: {
text: 'Hello, world!',
attachments: []
}
});

Send Custom Event

channel.trigger('cursor-move', {
x: 100,
y: 200,
user_id: 'user_123'
});

Unsubscribe

channel.unsubscribe();

Presence API

Track user presence and custom status in real-time.

Get Presence Members

const members = await channel.presence.getMembers();
// Returns array of currently present users

console.log(members);
// [
// { user_id: 'user_1', status: 'online', data: { name: 'Alice' } },
// { user_id: 'user_2', status: 'away', data: { name: 'Bob' } }
// ]

Track Presence

// Join with custom data
channel.presence.enter({
name: 'Alice',
avatar: 'https://example.com/alice.jpg',
status: 'online'
});

// Update presence data
channel.presence.update({
status: 'busy',
statusMessage: 'In a meeting'
});

// Leave presence
channel.presence.leave();

Presence Events

channel.on('presence:join', (member) => {
console.log(`${member.data.name} joined`);
});

channel.on('presence:leave', (member) => {
console.log(`${member.data.name} left`);
});

channel.on('presence:update', (member) => {
console.log(`${member.data.name} is now ${member.data.status}`);
});

channel.on('presence:sync', (members) => {
console.log('Full member list:', members);
});

Typing Indicators API

Show real-time typing status to other users.

Start Typing

channel.typing.start({
user_id: 'user_123',
user_name: 'Alice'
});

Stop Typing

channel.typing.stop();

Listen for Typing

channel.on('typing:start', (user) => {
console.log(`${user.user_name} is typing...`);
});

channel.on('typing:stop', (user) => {
console.log(`${user.user_name} stopped typing`);
});

Message History API

Persist and retrieve past messages.

Get Message History

GET /v1/realtime/channels/:channel_id/messages

Query Parameters:

ParameterTypeDescription
limitintegerMax messages to return (default: 50, max: 100)
beforestringGet messages before this message ID
afterstringGet messages after this message ID
orderstringSort order: asc or desc (default: desc)

Response:

{
"data": [
{
"id": "msg_abc123",
"type": "messages",
"attributes": {
"channel_id": "chat:room-1",
"user_id": "user_123",
"event": "message",
"data": {
"text": "Hello, everyone!",
"attachments": []
},
"created_at": "2024-01-15T10:30:00Z"
}
}
],
"meta": {
"has_more": true,
"oldest_id": "msg_xyz789",
"newest_id": "msg_abc123"
}
}

SDK Usage

// Get latest messages
const messages = await channel.history.fetch({
limit: 50
});

// Get older messages (pagination)
const olderMessages = await channel.history.fetch({
limit: 50,
before: 'msg_xyz789'
});

// Search messages
const searchResults = await client.realtime.messages.search({
channel_id: 'chat:room-1',
query: 'meeting',
limit: 20
});

Read Receipts API

Track message delivery and read status.

Mark as Read

POST /v1/realtime/channels/:channel_id/read

Request Body:

{
"data": {
"type": "read_receipt",
"attributes": {
"message_id": "msg_abc123",
"user_id": "user_456"
}
}
}

SDK Usage

// Mark messages as read
await channel.messages.markAsRead('msg_abc123');

// Get read status
const readStatus = await channel.messages.getReadStatus('msg_abc123');
// { read_by: ['user_123', 'user_456'], read_count: 2 }

Listen for Read Events

channel.on('message:read', (data) => {
console.log(`Message ${data.message_id} read by ${data.user_id}`);
});

WebRTC API

Peer-to-peer video, audio, and screen sharing.

Create a Room

POST /v1/realtime/webrtc/rooms

Request Body:

{
"data": {
"type": "webrtc_rooms",
"attributes": {
"name": "Team Meeting",
"max_participants": 10,
"recording_enabled": true,
"settings": {
"video": {
"enabled": true,
"quality": "hd"
},
"audio": {
"enabled": true,
"noise_suppression": true
},
"screen_sharing": {
"enabled": true
}
}
}
}
}

Response:

{
"data": {
"id": "room_abc123",
"type": "webrtc_rooms",
"attributes": {
"name": "Team Meeting",
"join_url": "https://meet.23blocks.com/room_abc123",
"token": "eyJ...",
"max_participants": 10,
"recording_enabled": true,
"settings": {...},
"created_at": "2024-01-15T10:30:00Z"
}
}
}

Join a Room

const room = await client.realtime.webrtc.join({
room_id: 'room_abc123',
token: 'room-token',
audio: true,
video: true
});

// Listen for participants
room.on('participant:joined', (participant) => {
console.log('Participant joined:', participant);
// Add their video stream to UI
addVideoElement(participant.stream);
});

room.on('participant:left', (participant) => {
console.log('Participant left:', participant);
removeVideoElement(participant.id);
});

Media Controls

// Toggle video
room.video.toggle();
room.video.enable();
room.video.disable();

// Toggle audio
room.audio.toggle();
room.audio.mute();
room.audio.unmute();

// Share screen
await room.screen.share();
room.screen.stop();

Leave Room

room.leave();

Broadcast API

Send messages from your server to all subscribers.

Publish to Channel

POST /v1/realtime/publish

Request Body:

{
"data": {
"type": "broadcast",
"attributes": {
"channel": "notifications:all",
"event": "announcement",
"data": {
"title": "System Maintenance",
"message": "Scheduled maintenance in 1 hour",
"type": "warning"
}
}
}
}

Batch Publish

POST /v1/realtime/publish/batch

Request Body:

{
"data": {
"type": "batch_broadcast",
"attributes": {
"messages": [
{
"channel": "notifications:user_123",
"event": "notification",
"data": { "message": "You have a new message" }
},
{
"channel": "notifications:user_456",
"event": "notification",
"data": { "message": "Your order shipped" }
}
]
}
}
}

Webhooks

Receive real-time events on your server.

Supported Events

EventDescription
channel.subscribedUser subscribed to a channel
channel.unsubscribedUser unsubscribed from a channel
message.sentMessage sent to a channel
presence.joinUser joined presence channel
presence.leaveUser left presence channel
presence.updateUser updated presence data
webrtc.room.createdWebRTC room created
webrtc.participant.joinedParticipant joined WebRTC room
webrtc.participant.leftParticipant left WebRTC room
webrtc.recording.readyRoom recording is ready

Webhook Payload

{
"id": "evt_abc123",
"type": "message.sent",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"channel_id": "chat:room-1",
"user_id": "user_123",
"message": {
"id": "msg_xyz789",
"text": "Hello, world!",
"created_at": "2024-01-15T10:30:00Z"
}
}
}

Configure Webhooks

POST /v1/realtime/webhooks

Request Body:

{
"data": {
"type": "webhooks",
"attributes": {
"url": "https://api.yourapp.com/webhooks/realtime",
"events": ["message.sent", "presence.join", "presence.leave"],
"secret": "whsec_...",
"active": true
}
}
}

Channel Authorization

Control access to private and presence channels.

Authorization Endpoint

When a user tries to subscribe to a private or presence channel, your server must authorize the request.

POST /your-app/auth/realtime

Your server receives:

{
"socket_id": "socket_abc123",
"channel_name": "private:team-123",
"user_id": "user_123"
}

Your server responds:

{
"auth": "app_id:signature",
"channel_data": {
"user_id": "user_123",
"user_info": {
"name": "Alice",
"avatar": "https://example.com/alice.jpg"
}
}
}

SDK Configuration

const realtime = await client.realtime.connect({
token: 'connection-token',
authEndpoint: 'https://api.yourapp.com/auth/realtime',
authHeaders: {
'Authorization': 'Bearer user-session-token'
}
});

Error Handling

Error Response Format

{
"errors": [
{
"status": "401",
"code": "unauthorized_channel",
"title": "Unauthorized",
"detail": "You are not authorized to subscribe to this channel"
}
]
}

Common Error Codes

StatusCodeDescription
400invalid_channelInvalid channel name format
401invalid_tokenConnection token is invalid or expired
401unauthorized_channelNot authorized to access this channel
403channel_limit_exceededMaximum channel subscriptions reached
404channel_not_foundChannel does not exist
429rate_limit_exceededToo many messages
500connection_failedFailed to establish connection

SDK Examples

TypeScript/JavaScript

import { create23BlocksClient } from '@23blocks/sdk';

const client = create23BlocksClient({
urls: { realtime: 'https://conversations.api.us.23blocks.com' },
appId: process.env.APP_ID,
apiKey: process.env.API_KEY,
});

// Initialize real-time connection
async function initRealtime(userId: string) {
// Get connection token
const { token } = await client.realtime.getToken({
user_id: userId,
channels: ['chat:*', `notifications:${userId}`]
});

// Connect
const realtime = await client.realtime.connect({ token });

// Handle connection events
realtime.on('connected', () => {
console.log('Connected!');
});

realtime.on('error', (error) => {
console.error('Connection error:', error);
});

return realtime;
}

// Chat room example
async function joinChatRoom(realtime: any, roomId: string, userId: string) {
const channel = realtime.subscribe(`presence:chat:${roomId}`);

// Enter presence
channel.presence.enter({
user_id: userId,
name: 'Alice',
avatar: 'https://example.com/alice.jpg'
});

// Listen for messages
channel.on('message', (message) => {
displayMessage(message);
});

// Listen for presence
channel.on('presence:join', (member) => {
addUserToList(member);
});

channel.on('presence:leave', (member) => {
removeUserFromList(member);
});

// Listen for typing
channel.on('typing:start', (user) => {
showTypingIndicator(user);
});

channel.on('typing:stop', (user) => {
hideTypingIndicator(user);
});

return channel;
}

// Send message
function sendMessage(channel: any, text: string) {
channel.send({
type: 'message',
data: {
text,
timestamp: new Date().toISOString()
}
});
}

React Chat Component

import React, { useEffect, useState, useRef } from 'react';
import { create23BlocksClient, RealtimeChannel } from '@23blocks/sdk';

interface Message {
id: string;
user_id: string;
user_name: string;
text: string;
created_at: string;
}

interface User {
user_id: string;
name: string;
avatar: string;
}

const client = create23BlocksClient({
urls: { realtime: 'https://conversations.api.us.23blocks.com' },
appId: process.env.REACT_APP_APP_ID!,
apiKey: process.env.REACT_APP_API_KEY!,
});

export function ChatRoom({ roomId, currentUser }: { roomId: string; currentUser: User }) {
const [messages, setMessages] = useState<Message[]>([]);
const [members, setMembers] = useState<User[]>([]);
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const [inputValue, setInputValue] = useState('');
const [isConnected, setIsConnected] = useState(false);
const channelRef = useRef<RealtimeChannel | null>(null);
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
let realtime: any;

async function connect() {
// Get token and connect
const { token } = await client.realtime.getToken({
user_id: currentUser.user_id,
channels: [`presence:chat:${roomId}`]
});

realtime = await client.realtime.connect({ token });

realtime.on('connected', () => {
setIsConnected(true);
});

// Subscribe to chat room
const channel = realtime.subscribe(`presence:chat:${roomId}`);
channelRef.current = channel;

// Enter presence
channel.presence.enter({
user_id: currentUser.user_id,
name: currentUser.name,
avatar: currentUser.avatar
});

// Get presence members
const currentMembers = await channel.presence.getMembers();
setMembers(currentMembers.map((m: any) => m.data));

// Load message history
const history = await channel.history.fetch({ limit: 50 });
setMessages(history.reverse());

// Listen for new messages
channel.on('message', (message: any) => {
setMessages(prev => [...prev, message.data]);
});

// Listen for presence changes
channel.on('presence:join', (member: any) => {
setMembers(prev => [...prev, member.data]);
});

channel.on('presence:leave', (member: any) => {
setMembers(prev => prev.filter(m => m.user_id !== member.data.user_id));
});

// Listen for typing indicators
channel.on('typing:start', (user: any) => {
if (user.user_id !== currentUser.user_id) {
setTypingUsers(prev => [...new Set([...prev, user.user_name])]);
}
});

channel.on('typing:stop', (user: any) => {
setTypingUsers(prev => prev.filter(name => name !== user.user_name));
});
}

connect();

return () => {
if (channelRef.current) {
channelRef.current.presence.leave();
channelRef.current.unsubscribe();
}
if (realtime) {
realtime.disconnect();
}
};
}, [roomId, currentUser]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);

// Send typing indicator
if (channelRef.current) {
channelRef.current.typing.start({
user_id: currentUser.user_id,
user_name: currentUser.name
});

// Clear previous timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}

// Stop typing after 2 seconds of inactivity
typingTimeoutRef.current = setTimeout(() => {
channelRef.current?.typing.stop();
}, 2000);
}
};

const sendMessage = () => {
if (!inputValue.trim() || !channelRef.current) return;

channelRef.current.send({
type: 'message',
data: {
id: `msg_${Date.now()}`,
user_id: currentUser.user_id,
user_name: currentUser.name,
text: inputValue,
created_at: new Date().toISOString()
}
});

channelRef.current.typing.stop();
setInputValue('');
};

return (
<div className="chat-room">
<div className="chat-header">
<span className={`status ${isConnected ? 'online' : 'offline'}`} />
<span>{members.length} online</span>
</div>

<div className="members-list">
{members.map(member => (
<div key={member.user_id} className="member">
<img src={member.avatar} alt={member.name} />
<span>{member.name}</span>
</div>
))}
</div>

<div className="messages">
{messages.map(message => (
<div
key={message.id}
className={`message ${message.user_id === currentUser.user_id ? 'own' : ''}`}
>
<strong>{message.user_name}</strong>
<p>{message.text}</p>
<small>{new Date(message.created_at).toLocaleTimeString()}</small>
</div>
))}
</div>

{typingUsers.length > 0 && (
<div className="typing-indicator">
{typingUsers.join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
</div>
)}

<div className="input-area">
<input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
}

React Video Call Component

import React, { useEffect, useRef, useState } from 'react';
import { create23BlocksClient } from '@23blocks/sdk';

interface Participant {
id: string;
user_id: string;
name: string;
stream: MediaStream;
audio: boolean;
video: boolean;
}

const client = create23BlocksClient({
urls: { realtime: 'https://conversations.api.us.23blocks.com' },
appId: process.env.REACT_APP_APP_ID!,
apiKey: process.env.REACT_APP_API_KEY!,
});

export function VideoRoom({ roomId, currentUser }: { roomId: string; currentUser: any }) {
const [participants, setParticipants] = useState<Participant[]>([]);
const [isAudioEnabled, setIsAudioEnabled] = useState(true);
const [isVideoEnabled, setIsVideoEnabled] = useState(true);
const [isScreenSharing, setIsScreenSharing] = useState(false);
const localVideoRef = useRef<HTMLVideoElement>(null);
const roomRef = useRef<any>(null);

useEffect(() => {
async function joinRoom() {
// Join WebRTC room
const room = await client.realtime.webrtc.join({
room_id: roomId,
audio: true,
video: true,
user_data: {
user_id: currentUser.user_id,
name: currentUser.name
}
});

roomRef.current = room;

// Set local video
if (localVideoRef.current && room.localStream) {
localVideoRef.current.srcObject = room.localStream;
}

// Handle participant events
room.on('participant:joined', (participant: Participant) => {
setParticipants(prev => [...prev, participant]);
});

room.on('participant:left', (participant: Participant) => {
setParticipants(prev => prev.filter(p => p.id !== participant.id));
});

room.on('participant:stream', (participant: Participant) => {
setParticipants(prev =>
prev.map(p => p.id === participant.id ? { ...p, stream: participant.stream } : p)
);
});

room.on('participant:audio', (participant: Participant) => {
setParticipants(prev =>
prev.map(p => p.id === participant.id ? { ...p, audio: participant.audio } : p)
);
});

room.on('participant:video', (participant: Participant) => {
setParticipants(prev =>
prev.map(p => p.id === participant.id ? { ...p, video: participant.video } : p)
);
});
}

joinRoom();

return () => {
if (roomRef.current) {
roomRef.current.leave();
}
};
}, [roomId, currentUser]);

const toggleAudio = () => {
if (roomRef.current) {
roomRef.current.audio.toggle();
setIsAudioEnabled(!isAudioEnabled);
}
};

const toggleVideo = () => {
if (roomRef.current) {
roomRef.current.video.toggle();
setIsVideoEnabled(!isVideoEnabled);
}
};

const toggleScreenShare = async () => {
if (roomRef.current) {
if (isScreenSharing) {
roomRef.current.screen.stop();
} else {
await roomRef.current.screen.share();
}
setIsScreenSharing(!isScreenSharing);
}
};

const leaveRoom = () => {
if (roomRef.current) {
roomRef.current.leave();
}
};

return (
<div className="video-room">
<div className="video-grid">
{/* Local video */}
<div className="video-tile local">
<video
ref={localVideoRef}
autoPlay
muted
playsInline
/>
<span className="name">{currentUser.name} (You)</span>
</div>

{/* Remote participants */}
{participants.map(participant => (
<ParticipantVideo
key={participant.id}
participant={participant}
/>
))}
</div>

<div className="controls">
<button
onClick={toggleAudio}
className={isAudioEnabled ? 'active' : 'muted'}
>
{isAudioEnabled ? 'Mute' : 'Unmute'}
</button>
<button
onClick={toggleVideo}
className={isVideoEnabled ? 'active' : 'disabled'}
>
{isVideoEnabled ? 'Stop Video' : 'Start Video'}
</button>
<button onClick={toggleScreenShare}>
{isScreenSharing ? 'Stop Share' : 'Share Screen'}
</button>
<button onClick={leaveRoom} className="leave">
Leave
</button>
</div>
</div>
);
}

function ParticipantVideo({ participant }: { participant: Participant }) {
const videoRef = useRef<HTMLVideoElement>(null);

useEffect(() => {
if (videoRef.current && participant.stream) {
videoRef.current.srcObject = participant.stream;
}
}, [participant.stream]);

return (
<div className="video-tile">
<video
ref={videoRef}
autoPlay
playsInline
/>
<span className="name">{participant.name}</span>
{!participant.audio && <span className="muted-icon">🔇</span>}
</div>
);
}

Conversations API — AI Summaries & Digest

The Conversations API adds AI-powered features on top of real-time messaging. Summaries and digests are generated by the Jarvis AI Block using LLM analysis.

Zero-Config AI Integration

Users are automatically registered with the Jarvis AI system on their first summary or digest request. No separate onboarding step is needed — just call the endpoint and it works.

AI Conversation Summary

Get a structured AI summary of a single conversation.

GET /conversations/:conversation_uid/summary

Headers:

Authorization: Bearer <user-jwt>
X-App-Id: your-app-id
X-Api-Key: your-api-key

Response:

{
"data": {
"id": "conv_abc123",
"type": "conversation_summary",
"attributes": {
"overall_summary": "Discussion about Q3 product roadmap with focus on AI features...",
"categories": ["product-planning", "ai-features", "timeline"],
"action_items": [
"Finalize AI summary API spec by Friday",
"Schedule design review for digest UI"
],
"key_decisions": [
"AI features prioritized for Q3 launch",
"Team agreed on incremental rollout strategy"
],
"sentiment": "positive",
"stats": {
"total_messages": 47,
"participants": 4,
"time_span": "2h 15m"
},
"generated_at": "2026-05-20T10:30:00Z",
"cached": false
}
}
}

How it works:

  • Incremental processing — only new messages since the last summary are sent to the LLM
  • Built-in caching with smart invalidation and staleness detection
  • 60-second rate limiting per conversation
  • Schema validation of all LLM outputs with automatic retry
  • Graceful degradation — stale cache returned if Jarvis is temporarily unavailable

AI Cross-Conversation Digest

Aggregate all unread conversations into a single AI-generated digest. One API call gives admins or users a complete picture of all active conversations.

GET /users/:user_uid/conversations/summary

Query Parameters:

ParameterTypeDescription
custom[key]stringFilter by JSONB payload field (e.g., ?custom[project]=alpha)

Response:

{
"data": {
"id": "digest_xyz789",
"type": "conversation_digest",
"attributes": {
"conversations_found": 12,
"digest": {
"summary": "12 active conversations across 3 projects. Key updates: deployment approved for Project Alpha, design review pending for Project Beta...",
"categories": {
"urgent": ["conv_001", "conv_005"],
"pending": ["conv_002", "conv_008"],
"on_track": ["conv_003", "conv_004", "conv_006"],
"stale": ["conv_007"]
},
"action_items": [
"Approve Project Beta design mockups",
"Assign bug #4521 to backend team",
"Follow up on stale casting inquiry"
],
"stats": {
"total_unread_messages": 89,
"conversations_with_unread": 8,
"oldest_unread": "2026-05-19T08:00:00Z"
}
},
"meta": {
"tokens_used": 2340,
"generated_at": "2026-05-20T10:30:00Z",
"cache_status": "fresh"
}
}
}
}

The digest categorizes conversations by urgency: urgent, pending, on_track, and stale.

How it works:

  • Role-aware grouping across conversations
  • AI categorization by urgency (urgent/pending/on_track/stale) with prioritized action items
  • Progressive warming — handles large inboxes without timeouts (caps 10 fresh per request)
  • Per-tenant configuration: recency_days, max_fresh_per_request, unread_only
  • Per-tenant prompt configuration via company settings (customizable LLM prompts)
  • JSONB payload filtering for scoping digests to specific projects or tags
  • Graceful degradation — stale cache returned if Jarvis is temporarily unavailable

Use Cases

IndustryHow AI Summaries Help
Customer SupportCategorize conversations by urgency, extract action items for agents
EducationSummarize student-teacher conversations with follow-up reminders
MarketplaceTrack talent casting inquiries across projects with status categories
AI Agent PlatformsFeed conversation digests to downstream AI agents for automated triage

Unread Summary

Get unread conversation counts with grouping and filtering options.

GET /users/:user_uid/unread-summary

Query Parameters:

ParameterTypeDescription
has_unreadbooleantrue (default) returns only conversations with unread messages. false returns all conversations with counts.

Response:

{
"data": {
"id": "usr_abc123",
"type": "unread_summary",
"attributes": {
"total_unread": 89,
"conversations": [
{
"conversation_uid": "conv_001",
"unread_count": 15,
"last_message_at": "2026-05-20T10:25:00Z"
},
{
"conversation_uid": "conv_002",
"unread_count": 7,
"last_message_at": "2026-05-20T09:45:00Z"
}
]
}
}
}

Key improvements:

  • Per-conversation unread counts (previously group-level only)
  • Role-aware unread tracking via notify_roles on conversations
  • Flexible filtering with has_unread parameter

Auto-Registration with Jarvis

When a user calls a summary or digest endpoint for the first time, the system automatically registers them with the Jarvis AI Block. This is transparent — consumers don't need to call any registration endpoint or add any onboarding steps.

The auto-registration uses a register-once pattern with transparent retry. Subsequent requests skip registration entirely.


Rate Limits

PlanConnectionsMessages/secChannelsHistory Retention
Free100501024 hours
Starter5,0005001007 days
Pro50,0005,0001,00030 days
EnterpriseUnlimitedCustomUnlimitedCustom

Best Practices

Optimize Connection Handling

// Use a singleton for real-time connection
class RealtimeService {
private static instance: any = null;

static async getInstance(userId: string) {
if (!this.instance) {
const { token } = await client.realtime.getToken({ user_id: userId });
this.instance = await client.realtime.connect({ token });

// Handle reconnection
this.instance.on('disconnected', async () => {
// Token might be expired, get new one
const { token: newToken } = await client.realtime.getToken({ user_id: userId });
await this.instance.reconnect({ token: newToken });
});
}
return this.instance;
}
}

Handle Offline Messages

// Queue messages when offline
const messageQueue: any[] = [];

realtime.on('disconnected', () => {
// Switch to queueing mode
});

realtime.on('connected', async () => {
// Send queued messages
for (const msg of messageQueue) {
await channel.send(msg);
}
messageQueue.length = 0;
});

Implement Typing Indicators Efficiently

// Debounce typing indicators
let typingTimeout: NodeJS.Timeout | null = null;

function handleTyping() {
if (!typingTimeout) {
channel.typing.start();
}

if (typingTimeout) {
clearTimeout(typingTimeout);
}

typingTimeout = setTimeout(() => {
channel.typing.stop();
typingTimeout = null;
}, 2000);
}

Changelog

v3.0.0

  • Added AI Conversation Summaries (LLM-powered, incremental, cached)
  • Added AI Cross-Conversation Digest (aggregate unread into single digest)
  • Added Smart Unread Tracking (per-conversation counts, role-aware)
  • Added auto-registration with Jarvis AI system
  • Added JSONB payload filtering for digest scoping

v2.1.0

  • Added WebRTC screen sharing
  • Improved reconnection logic
  • Added message search API

v2.0.0

  • Complete API redesign
  • Added WebRTC support
  • Added typing indicators and read receipts
  • Enhanced presence tracking

v1.0.0

  • Initial release with WebSocket channels and basic presence