Voisnap Docs
Guides

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

HubPathUse for
ChatHub/hubs/chatText conversations
VoiceHub/hubs/voiceStreaming 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 the on(...) handlers.
  • React — hold the client in a useRef, start()/stop() in an effect keyed by agent + auth, and push hub events into state via the on(...) handlers.

See the package README and the Real-time overview for the complete event contracts.

On this page