Logo
Search
API Docs

Web SDK Integration

Chat Agents

Web SDK & Client-Side Voice Integration

Overview

The Web SDK lets you build a fully custom, browser-based voice interface for your assistant — your own UI, your own call controls, your own visual state. This is different from the pre-built embeddable widget: the widget gives you a ready-made chat/voice bubble to drop onto a page, while the Web SDK gives you the raw building blocks (a JavaScript client, lifecycle events, and hooks for injecting context) to construct a voice experience from scratch.


Installation & Starting a Call

Install the client package with your preferred package manager:

npm install @core-system/web-sdk

Initialize it with your public API key, then start a call against an assistant ID:

import SulusVoice from '@core-system/web-sdk';

const sulusVoice = new SulusVoice('YOUR_PUBLIC_API_KEY');

// Start a voice conversation
sulusVoice.start('YOUR_ASSISTANT_ID');

End the call from your own UI with:

sulusVoice.stop();

Listening to Events

Subscribe to lifecycle and message events with .on(eventName, callback):

EventDescription
call-startFired when the call begins
call-endFired when the call ends
speech-startFired when the assistant starts speaking
speech-endFired when the assistant stops speaking
messageFired for transcripts, tool-calls, and other message types
errorFired when an error occurs
sulusVoice.on('call-start', () => console.log('Call started'));
sulusVoice.on('call-end', () => console.log('Call ended'));

sulusVoice.on('speech-start', () => console.log('Assistant speaking'));
sulusVoice.on('speech-end', () => console.log('Assistant stopped speaking'));

sulusVoice.on('message', (message) => {
  if (message.type === 'transcript') {
    console.log(`${message.role}: ${message.transcript}`);
  }
});

sulusVoice.on('error', (error) => console.error('Error:', error));

sulusVoice.start('YOUR_ASSISTANT_ID');

Client-Side Tools

If you define a tool without a server URL, the Web SDK emits tool-calls messages that your frontend handles directly, instead of routing the call to your backend. Include 'tool-calls' in clientMessages to receive these:

sulusVoice.start({
  model: {
    provider: 'openai',
    model: 'gpt-4.1',
    tools: [{ type: 'function', function: { name: 'updateUI', /* ... */ } }],
  },
  clientMessages: ['tool-calls'],
});

sulusVoice.on('message', (message) => {
  if (message.type === 'tool-calls') {
    message.toolCallList.forEach((toolCall) => {
      if (toolCall.function?.name === 'updateUI') {
        // Handle the UI update here
      }
    });
  }
});

Client-side tools cannot send a result back to the model. If the model needs the tool's output to keep reasoning, use a server-based tool instead.


Injecting Context Mid-Call

Use addMessage() to feed the assistant additional context while a call is already in progress — useful for passing in state your UI already knows about, without waiting for the caller to say it out loud:

sulusVoice.addMessage({
  role: 'system',
  content: 'User is on the premium plan.',
});

Live Captions with speechStarted

For a captions UI, subscribe to the opt-in assistant.speechStarted event by adding it to clientMessages:

sulusVoice.start({
  // ...
  clientMessages: ['assistant.speechStarted', 'transcript'],
});

sulusVoice.on('message', (message) => {
  if (message.type !== 'assistant.speechStarted') return;
  // message.text holds the full text for the current turn (not a delta)
  // message.turn is a 0-indexed turn counter
  // message.timing, when present, carries word-level timestamps for providers that support it
});

Behavior worth knowing before you build on this event:

  • text accumulates across events within the same turn — it is not a delta, so re-render rather than append.
  • Word-level timing data is only present for voice providers that support it; other providers emit text-only events, one per speech chunk.
  • On caller barge-in, no further events fire for the interrupted turn — pair this with your interruption handling to know what was actually spoken.
  • There is no corresponding "speech stopped" event; use turn changes or your existing speech-state events to detect end-of-turn.