Build Your Own Widget
Own the entire widget UI by building directly on the Voisnap realtime SignalR hubs with the @voisnap/widget-core package — typed hub contracts, pass-through auth, session persistence, and Web-Audio handling.
Build Your Own Widget
The embeddable widget covers most needs through three tiers: theme tokens
(Tier 1), ui_config labels/behavior
(Tier 2), and — when you want total control of the interface — building directly on the
realtime hubs (Tier 3). This guide covers Tier 3.
You don't have to re-derive the protocol: the @voisnap/widget-core package ships the
typed hub contracts and the hard parts of a voice UI (Web-Audio PCM capture/playback,
barge-in). The built-in widget is the canonical reference implementation.
npm i @voisnap/widget-core @microsoft/signalr
The hubs
| Hub | Path | Use for |
|---|---|---|
| ChatHub | /hubs/chat | Text conversations |
| VoiceHub | /hubs/voice | Streaming voice (16 kHz in / 24 kHz out PCM) |
Connect with the agent + caller context on the query string:
/hubs/chat?agentId=<agentKey>&userName=<name>&language=<bcp47>&auth=true
auth=true tells the runtime a pass-through credential will arrive over the hub (so the
system prompt is rendered for an authenticated user).
See Real-time → Chat Hub and Voice Hub for the full event reference.
Chat
import { HubConnectionBuilder } from '@microsoft/signalr';
import { VoisnapChatClient, buildHubUrl } from '@voisnap/widget-core';
const url = buildHubUrl('https://api.voisnap.ai', 'chat', {
agentId: 'your-agent-key',
userName: 'Alex',
language: 'English',
auth: true,
});
const conn = new HubConnectionBuilder().withUrl(url).withAutomaticReconnect().build();
const chat = new VoisnapChatClient(conn);
chat.on('connected', (sessionId) => persist(sessionId));
chat.on('messageReceived', (text, isFinal) => render(text, isFinal));
chat.on('toolCallStarted', (calls) => showSpinner(calls));
chat.on('sessionExpired', () => reauthenticate()); // token expired server-side
chat.on('error', (msg) => console.error(msg));
await chat.start();
await chat.setTokens({ accessToken, refreshToken, expiresAtUtc }); // optional pass-through
await chat.server.SendMessage('Hi!');
Pass-through auth
The agent's tools can call your backend on behalf of the logged-in user. Connect with
auth: true, then push the user's session token over the hub with setTokens(). Always
supply expiresAtUtc — without it the runtime treats the token as stale and drops it. Listen
for sessionExpired and call updateTokens() with a fresh pair. Tokens live only in the
runtime session and are never persisted.
await chat.setTokens({ accessToken, refreshToken, expiresAtUtc: '2026-06-01T12:00:00Z' });
chat.on('sessionExpired', async () => {
const next = await refreshFromYourBackend();
await chat.updateTokens({ accessToken: next.token, expiresAtUtc: next.expiresAt });
});
Session persistence
connected / reconnected return the sessionId. Persist it (e.g. sessionStorage) and use
withAutomaticReconnect() so a dropped socket resumes the same conversation instead of
starting a new one. The server resumes a parked session on reconnect within its idle window
(~30 minutes); after that a fresh session begins.
Voice & audio
Voice is the part worth not reinventing. The VoiceHub speaks raw PCM16, base64-encoded:
16 kHz mono in (SendAudio), 24 kHz mono out (audioOutput). @voisnap/widget-core
provides the conversions and a gap-less playback scheduler.
import {
VoisnapVoiceClient, PcmPlayer, floatTo16BitPcm, arrayBufferToBase64,
resample, INPUT_SAMPLE_RATE,
} from '@voisnap/widget-core';
const ctx = new AudioContext();
const player = new PcmPlayer(ctx); // schedules 24 kHz output back-to-back
const voice = new VoisnapVoiceClient(conn);
voice.on('audioOutput', (b64) => player.enqueue(b64));
voice.on('transcription', (t) => showCaption(t));
voice.on('sessionEnded', (reason) => cleanup(reason));
// Mic capture → 16 kHz PCM16 → base64 → SendAudio (use an AudioWorklet in production):
function onMicFrame(frame: Float32Array, micRate: number) {
const pcm = floatTo16BitPcm(resample(frame, micRate, INPUT_SAMPLE_RATE));
void voice.server.SendAudio(arrayBufferToBase64(pcm));
}
// Barge-in: stop playback immediately and tell the server the user is interrupting.
function onUserSpeaks() {
player.stop();
void voice.server.BargeIn();
}
Other VoiceHub methods: SendText, SendDtmf, EndSession, and the same token methods as
chat.
Framework starters
The package is framework-agnostic. Copy-pasteable starters ship in the package's examples/
folder:
examples/react/VoisnapChatWidget.tsx— React text chat (connect, stream, send, auth).examples/react/VoisnapVoiceWidget.tsx— React voice (mic → 16 kHz PCM, gap-less playback, barge-in).examples/vanilla/— framework-free chat (HTML + ES module).
The patterns in brief:
- Vanilla — instantiate the client in a module, wire DOM events to
server.*calls, and render from theon(...)handlers. - React — hold the client in a
useRef,start()/stop()in an effect keyed by agent + auth, and push hub events into state via theon(...)handlers.
See the package README and the Real-time overview for the complete
event contracts.