Add a voice agent to your web app with top-level apiKey/appId token-issuer identifiers, backend-issued tokens, router-aware navigation, and optional page automation.
⚠️ Beta Release — This open-source release is in beta. You may encounter rough edges, incomplete features, or breaking changes. We are actively reviewing and merging community PRs, but please expect some instability as we iterate toward a stable release. Your feedback and contributions are welcome.
- Realtime browser client for voice sessions
- Router adapters for Next.js, TanStack Router, React Router, Vue Router, and custom navigation
- Optional automation adapter for DOM interaction
- React bindings via
@vowel.to/client/react - Web component and standalone bundle via
@vowel.to/client/standalone
npm install @vowel.to/clientimport { Vowel, createNextJSAdapters } from '@vowel.to/client';
import { useRouter } from 'next/navigation';
const router = useRouter();
const { navigationAdapter, automationAdapter } = createNextJSAdapters(router, {
routes: [
{ path: '/', description: 'Home page' },
{ path: '/products', description: 'Product catalog' },
{ path: '/cart', description: 'Shopping cart' },
],
enableAutomation: true,
});
const vowel = new Vowel({
apiKey: 'vkey_public_xxx',
navigationAdapter,
automationAdapter,
language: 'en-US',
turnDetectionPreset: 'balanced',
initialGreetingPrompt:
'Introduce yourself as a helpful assistant for this store and ask how you can help.',
instructions: `
You are a helpful shopping assistant.
- Keep spoken responses concise and conversational.
- Do not use markdown in spoken responses.
- Translate non-English requests before calling tools if needed.
`,
});
vowel.registerAction(
'searchProducts',
{
description: 'Search for products in the catalog',
parameters: {
query: { type: 'string', description: 'Search query in English' },
},
},
async ({ query }) => {
return { success: true, query };
}
);
await vowel.startSession();Register actions before calling startSession().
Prefer apiKey at the top level. During the transition away from hosted appId auth, apiKey and appId are aliases and either field may contain either a publishable API key or a legacy appId.
const vowel = new Vowel({
apiKey: 'vkey_public_xxx',
language: 'en-US',
initialGreetingPrompt: 'Welcome the user and ask how you can help.',
turnDetectionPreset: 'balanced',
});Use tokenProvider when your backend or self-hosted token service decides whether a browser session can start.
const vowel = new Vowel({
apiKey: 'vkey_public_xxx',
tokenProvider: async ({ apiKey, origin, config }) => {
const response = await fetch('/api/vowel/token', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
},
body: JSON.stringify({ origin, config }),
});
if (!response.ok) {
throw new Error('Unable to fetch session token');
}
return response.json();
},
language: 'en-US',
initialGreetingPrompt: 'Welcome the user and ask what they want to do next.',
turnDetectionPreset: 'balanced',
_voiceConfig: {
provider: 'vowel-prime',
vowelPrimeConfig: {
endpointPreset: 'staging',
},
},
});Direct tokens are still supported for migration and advanced setups via _voiceConfig.token, but new examples should prefer tokenProvider.
const vowel = new Vowel({
language: 'en-US',
_voiceConfig: {
provider: 'vowel-prime',
token: 'your-ephemeral-token',
},
});Use top-level fields for the public browser-facing config:
const vowel = new Vowel({
apiKey: 'vkey_public_xxx',
language: 'en-US',
initialGreetingPrompt: 'Welcome the user and ask how you can help.',
turnDetectionPreset: 'balanced',
instructions: 'Keep answers brief and conversational.',
});Use _voiceConfig for backend/runtime overrides such as provider, model, voice, token, and advanced turn detection:
const vowel = new Vowel({
apiKey: 'vkey_public_xxx',
language: 'en-US',
initialGreetingPrompt: 'Welcome the user and ask how you can help.',
turnDetectionPreset: 'balanced',
_voiceConfig: {
provider: 'vowel-prime',
llmProvider: 'groq',
model: 'openai/gpt-oss-120b',
voice: 'Timothy',
vowelPrimeConfig: {
environment: 'staging',
},
turnDetection: {
mode: 'client_vad',
},
},
});Notes:
voiceConfigstill exists for legacy compatibility, but_voiceConfigis the preferred field.- Hosted apps should not hardcode hosted preset internals in the client; select managed presets in hosted vowel.
- Self-hosted
coredeployments can own the full runtime JSON and issue tokens from their own service.
Vowel separates navigation from page automation.
navigationAdapter: where to goautomationAdapter: what to do on the page
Helper factories:
import {
createDirectAdapters,
createControlledAdapters,
createTanStackAdapters,
createNextJSAdapters,
createVueRouterAdapters,
createReactRouterAdapters,
} from '@vowel.to/client';Common usage:
createTanStackAdapters(...)for TanStack Router appscreateNextJSAdapters(...)for Next.js appscreateReactRouterAdapters(...)for React Router appscreateControlledAdapters(...)for traditional multi-page sites
If you omit automationAdapter, navigation and custom actions still work.
import { VowelProvider, VowelAgent } from '@vowel.to/client/react';
export function App() {
return (
<VowelProvider client={vowel}>
<YourRoutes />
<VowelAgent position="bottom-right" />
</VowelProvider>
);
}await vowel.startSession();
await vowel.pauseSession();
await vowel.resumeSession();
await vowel.sendText('What can I do on this page?');
await vowel.notifyEvent('Order placed successfully!', { orderId: '12345' });
const state = vowel.exportState({ maxTurns: 20 });
await vowel.startSession({ restoreState: state });<script src="https://cdn.vowel.to/vowel-voice-widget.min.js"></script>
<vowel-voice-widget
app-id="your-app-id"
preset="controlled"
position="bottom-right"
show-transcripts="true">
</vowel-voice-widget>Use the config attribute for JSON configuration overrides when embedding the widget.
Documentation is now hosted at docs.vowel.to:
- Client Quick Reference
- Pause, Resume & State Restoration
- Connection Paradigms
- Demo Example (React)
- Demo Example (Next.js)
- Node.js 18+ or Bun
- A modern browser with microphone access
- HTTPS for microphone access outside localhost
Proprietary. See client/package.json for package metadata.