Real-Time APIs
ChatHub
Real-time text chat with Voisnap agents via SignalR — send messages, receive responses, and handle typing indicators.
ChatHub
ChatHub provides real-time text-based chat between users and Voisnap agents. It powers the web chat widget and can be used to embed AI chat into any web or mobile application.
Connection
wss://api.voisnap.ai/hubs/chat?agentId={agentId}&access_token={jwt}
Optional query parameters:
| Parameter | Description |
|---|---|
sessionId | Resume a previous chat session |
metadata | URL-encoded JSON metadata passed to the agent |
language | Override detected language (BCP 47 code, e.g. es-MX) |
Client → Server methods
SendMessageAsync
Send a text message from the user.
await connection.invoke("SendMessageAsync", {
content: "Hi, I need help with my account",
metadata: { userId: "user_12345" }
});
TypingAsync
Send a typing indicator (call repeatedly while user is typing).
await connection.invoke("TypingAsync");
CancelAsync
Cancel the agent's current response generation (if the agent is mid-reply).
await connection.invoke("CancelAsync");
EndSessionAsync
End the chat session.
await connection.invoke("EndSessionAsync");
Server → Client events
MessageReceived
The agent's reply. May arrive in streaming chunks (isChunk: true) followed by a final message.
connection.on("MessageReceived", (data) => {
// data: {
// messageId: string,
// content: string,
// isChunk: boolean,
// isFinal: boolean,
// timestamp: string
// }
if (data.isFinal) {
appendFinalMessage('agent', data.content);
} else {
updateStreamingMessage(data.messageId, data.content);
}
});
TypingIndicator
The agent is generating a response.
connection.on("TypingIndicator", (data) => {
// data: { isTyping: boolean }
showTypingIndicator(data.isTyping);
});
ConversationEnded
connection.on("ConversationEnded", (data) => {
// data: { conversationId: string, reason: string }
console.log(`Chat ended: ${data.reason}`);
connection.stop();
});
Error
connection.on("Error", (data) => {
console.error(`[${data.code}] ${data.message}`);
});
React hook example
import { useEffect, useRef, useState, useCallback } from 'react';
import * as signalR from '@microsoft/signalr';
interface Message {
id: string;
role: 'user' | 'agent';
content: string;
timestamp: Date;
}
export function useVoisnapChat(agentId: string, accessToken: string) {
const connectionRef = useRef<signalR.HubConnection | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [isConnected, setIsConnected] = useState(false);
const [agentTyping, setAgentTyping] = useState(false);
const streamingRef = useRef<Map<string, string>>(new Map());
useEffect(() => {
const connection = new signalR.HubConnectionBuilder()
.withUrl(
`wss://api.voisnap.ai/hubs/chat?agentId=${agentId}&access_token=${accessToken}`
)
.withAutomaticReconnect()
.build();
connection.on('MessageReceived', (data) => {
if (!data.isFinal) {
// Stream chunk — accumulate
streamingRef.current.set(
data.messageId,
(streamingRef.current.get(data.messageId) ?? '') + data.content
);
setMessages(prev => {
const idx = prev.findIndex(m => m.id === data.messageId);
const accumulated = streamingRef.current.get(data.messageId) ?? '';
if (idx >= 0) {
return prev.map((m, i) =>
i === idx ? { ...m, content: accumulated } : m
);
}
return [...prev, {
id: data.messageId,
role: 'agent',
content: accumulated,
timestamp: new Date(data.timestamp),
}];
});
} else {
streamingRef.current.delete(data.messageId);
}
});
connection.on('TypingIndicator', d => setAgentTyping(d.isTyping));
connection.on('ConversationEnded', () => {
setIsConnected(false);
connection.stop();
});
connection.start()
.then(() => setIsConnected(true))
.catch(console.error);
connectionRef.current = connection;
return () => { connection.stop(); };
}, [agentId, accessToken]);
const sendMessage = useCallback(async (content: string) => {
if (!connectionRef.current || !isConnected) return;
const userMsg: Message = {
id: `user-${Date.now()}`,
role: 'user',
content,
timestamp: new Date(),
};
setMessages(prev => [...prev, userMsg]);
await connectionRef.current.invoke('SendMessageAsync', { content });
}, [isConnected]);
const endSession = useCallback(async () => {
await connectionRef.current?.invoke('EndSessionAsync');
}, []);
return { messages, isConnected, agentTyping, sendMessage, endSession };
}
// Usage in a component:
function ChatWidget({ agentId, accessToken }: { agentId: string; accessToken: string }) {
const { messages, isConnected, agentTyping, sendMessage } = useVoisnapChat(agentId, accessToken);
const [input, setInput] = useState('');
return (
<div className="chat-widget">
<div className="messages">
{messages.map(msg => (
<div key={msg.id} className={`message ${msg.role}`}>
{msg.content}
</div>
))}
{agentTyping && <div className="typing-indicator">Agent is typing...</div>}
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && input.trim()) {
sendMessage(input.trim());
setInput('');
}
}}
placeholder={isConnected ? 'Type a message...' : 'Connecting...'}
disabled={!isConnected}
/>
<button onClick={() => { sendMessage(input.trim()); setInput(''); }}>
Send
</button>
</div>
</div>
);
}