The @xuna-ai/react package gives you hooks and providers to build completely custom voice interfaces in React. Use it when you need control the widget doesn’t provide — custom UI, client-side tools, dynamic overrides, or fine-grained state management.
Installation
npm install @xuna-ai/react
pnpm install @xuna-ai/react
@xuna-ai/react re-exports everything from @xuna-ai/client, so you only need one dependency.
Minimal example
Wrap your app in ConversationProvider, then use hooks inside it to start and stop sessions.
import {
ConversationProvider,
useConversationControls,
useConversationStatus,
} from '@xuna-ai/react';
function App() {
return (
<ConversationProvider>
<Agent />
</ConversationProvider>
);
}
function Agent() {
const { startSession, endSession } = useConversationControls();
const { status } = useConversationStatus();
if (status === 'connected') {
return <button onClick={endSession}>End conversation</button>;
}
return (
<button
onClick={() =>
startSession({ agentId: 'agent_7101k5zvyjhmfg983brhmhkd98n6' })
}
>
Start conversation
</button>
);
}
ConversationProvider
Place ConversationProvider at the root of any component tree that needs access to conversation state. It accepts the following props:
| Prop | Type | Description |
|---|
onConnect | () => void | Called when the session connects. |
onDisconnect | () => void | Called when the session ends. |
onError | (error: Error) => void | Called on session errors. |
clientTools | Record<string, Function> | Tool handlers the agent can invoke on the client. |
serverLocation | string | Region for the conversation server (see Server location). |
isMuted | boolean | Controlled mute state for the microphone. |
onMutedChange | (muted: boolean) => void | Called when mute state changes. |
Hooks
Use granular hooks to avoid unnecessary re-renders. Each hook subscribes only to the state it needs.
useConversationControls
Session lifecycle and messaging actions.
const {
startSession,
endSession,
sendUserMessage,
sendContextualUpdate,
setVolume,
sendFeedback,
} = useConversationControls();
| Function | Description |
|---|
startSession(options) | Start a new conversation session. |
endSession() | End the current session. |
sendUserMessage(text) | Send a text message as the user. |
sendContextualUpdate(text) | Inject background context without creating a visible message. |
setVolume(volume) | Set agent output volume (0–1). |
sendFeedback(feedback) | Submit thumbs-up / thumbs-down feedback for the session. |
useConversationStatus
Current connection state.
const { status, message } = useConversationStatus();
// status: 'disconnected' | 'connecting' | 'connected'
Microphone mute control.
const { isMuted, setMuted } = useConversationInput();
useConversationMode
Whether the agent is speaking or listening.
const { mode, isSpeaking, isListening } = useConversationMode();
useConversationFeedback
Submit session feedback.
const { canSendFeedback, sendFeedback } = useConversationFeedback();
Register a client tool from inside a component. The tool name must match what you configured for the agent in the dashboard.
useConversationClientTool('showProductCard', (params: { productId: string }) => {
// render a product card in your UI
setSelectedProduct(params.productId);
return 'Product card displayed';
});
useConversation
Combines all hooks in one. Convenient for simple components, but causes more re-renders than using granular hooks.
const conversation = useConversation();
Prefer the granular hooks (useConversationControls, useConversationStatus, etc.) in performance-sensitive components. Use useConversation only in small or infrequent-rendering components.
startSession options
Pass options to startSession to control how the session connects.
| Option | Description |
|---|
agentId | Agent ID for public agents. |
signedUrl | Signed WebSocket URL for private agents (WebSocket auth). |
conversationToken | Token for private agents (WebRTC auth). |
userId | Optional end-user identifier for analytics and personalization. |
Authenticating private agents
If your agent is private, generate a credential on your server and pass it to startSession. Choose WebSocket (signed URL) or WebRTC (conversation token) based on your stack.
Serverapp.get('/signed-url', yourAuthMiddleware, async (req, res) => {
const response = await fetch(
`https://api.xuna.ai/v1/convai/conversation/get-signed-url?agent_id=${process.env.AGENT_ID}`,
{ headers: { 'xi-api-key': process.env.XUNA_AI_API_KEY } }
);
const body = await response.json();
res.send(body.signed_url);
});
Clientconst { startSession } = useConversationControls();
async function connect() {
const response = await fetch('/signed-url', yourAuthHeaders);
const signedUrl = await response.text();
await startSession({ signedUrl });
}
Serverapp.get('/conversation-token', yourAuthMiddleware, async (req, res) => {
const response = await fetch(
`https://api.xuna.ai/v1/convai/conversation/token?agent_id=${process.env.AGENT_ID}`,
{ headers: { 'xi-api-key': process.env.XUNA_AI_API_KEY } }
);
const body = await response.json();
res.send(body.token);
});
Clientconst { startSession } = useConversationControls();
async function connect() {
const response = await fetch('/conversation-token', yourAuthHeaders);
const conversationToken = await response.text();
await startSession({ conversationToken });
}
Never include your XUNA_AI_API_KEY in client-side code. Always call the XUNA AI API from your server.
Client tools let the agent call functions in your React app during a conversation — for example, to display information, submit a form, or navigate the UI. Define them on ConversationProvider or register them with useConversationClientTool.
<ConversationProvider
clientTools={{
displayMessage: (parameters: { text: string }) => {
alert(parameters.text);
return 'Message displayed';
},
}}
>
<Agent />
</ConversationProvider>
The return value is sent back to the agent as the tool result.
Overrides
Pass overrides to startSession to customize agent behavior for a specific session without changing the agent’s dashboard configuration.
await startSession({
agentId: 'agent_7101k5zvyjhmfg983brhmhkd98n6',
overrides: {
agent: {
prompt: { prompt: 'You are a helpful assistant for Acme Corp.' },
firstMessage: 'Hi! How can I help you today?',
language: 'en',
},
tts: {
voiceId: 'your_custom_voice_id',
},
},
});
Server location
By default, conversation servers are located in the US. Set serverLocation on ConversationProvider to use a different region.
| Value | Region |
|---|
"us" | United States (default) |
"eu-residency" | European Union |
"in-residency" | India |
"global" | Closest available region |
<ConversationProvider serverLocation="eu-residency">
<Agent />
</ConversationProvider>
Next steps
- To deploy without a custom UI, use the widget instead.
- For mobile apps, see the Mobile SDKs page.
- To build a fully custom integration outside React, use the WebSocket API.