Human Handover Example
End-to-end example of building a human handover flow where a live operator takes over a Virbe conversation from the virtual being.
This guide walks through building a system where a customer interacts with a virtual being, and at any point a human operator can take over the conversation. It is based on the Virbe React Integration Demo.
Use Case
A typical human handover setup involves:
- Customer-facing display – shows the virtual being widget (web avatar, avatarless, or kiosk)
- Operator panel – a separate window or device where the human agent monitors and can take control
- Shared state – a mechanism (e.g. localStorage, a backend, or WebSocket) to synchronize the handover flag between views
When the operator activates handover mode, the widget stops its automated conversation and the operator communicates with the customer through the API.
Architecture Overview
┌─────────────────────┐ localStorage ┌─────────────────────┐
│ Customer Display │ ◄──────────────────► │ Operator Panel │
│ (Web Widget) │ │ (Admin UI) │
│ │ │ │
│ - Shows avatar │ │ - Toggle handover │
│ - Plays speech │ │ - View status │
│ - Handles events │ │ - Send messages │
└─────────────────────┘ └─────────────────────┘
│ │
│ Virbe Backend │
└──────────────► API ◄─────────────────────────┘Dashboard Configuration
When using programmatic conversation control, make sure these settings are disabled in your Profile's dashboard configuration:
- Auto-start on widget focused –
false - Auto-start on page focused –
false - Send signal on new conversation –
false
This ensures your application has full control over when conversations start and stop.
Step 1 – Shared State
The demo uses localStorage as a simple cross-window communication channel. Any tab on the same origin can read and write these flags:
// localStorage keys
const STORAGE_KEYS = {
humanHandover: 'HumanHandover',
controllerApiStatus: 'ControllerApiStatus',
};
// Simple reactive atom for React components
function useLocalStorageFlag(key: string): boolean {
const [value, setValue] = useState(
() => localStorage.getItem(key) === 'true'
);
useEffect(() => {
function onStorage(event: StorageEvent) {
if (event.key === key) {
setValue(event.newValue === 'true');
}
}
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, [key]);
return value;
}Step 2 – Customer-Facing Widget
The customer display renders the widget and reacts to the handover flag:
function WebAvatarView() {
const pluginRef = useRef<VirbePluginMethods>(null);
const humanHandover = useLocalStorageFlag('HumanHandover');
useEffect(() => {
if (!pluginRef.current) return;
const unsubInit = pluginRef.current.subscribe('onInitialized', () => {
console.log('Widget initialized');
});
const unsubStatus = pluginRef.current.subscribe('onApiStatusChanged', (event) => {
// Store status so the operator panel can see it
localStorage.setItem('ControllerApiStatus', JSON.stringify(event.detail.action));
if (event.detail.action === 'connected' && !humanHandover) {
pluginRef.current?.unmute();
pluginRef.current?.sendSignal('conversation-start');
}
});
return () => {
unsubInit();
unsubStatus();
};
}, [humanHandover]);
// When handover is active, stop the automated conversation
useEffect(() => {
if (humanHandover) {
pluginRef.current?.interruptSpeech();
pluginRef.current?.stopConversation();
}
}, [humanHandover]);
return (
<VirbePluginWrapper
ref={pluginRef}
profileId={import.meta.env.VITE_PROFILE_ID}
profileSecret={import.meta.env.VITE_PROFILE_SECRET}
dashboardUrl={import.meta.env.VITE_DASHBOARD_URL}
/>
);
}Key behaviors:
- When
humanHandoverisfalse– the widget runs normally, auto-connecting and starting conversations. - When
humanHandoveristrue– the widget interrupts any current speech and stops the conversation. The operator takes over via the API.
Step 3 – Operator Panel
The operator panel runs in a separate browser window. It toggles the handover flag and shows connection status:
function AdminScreen() {
const [handover, setHandover] = useState(
() => localStorage.getItem('HumanHandover') === 'true'
);
const toggleHandover = () => {
const newValue = !handover;
setHandover(newValue);
localStorage.setItem('HumanHandover', String(newValue));
};
const apiStatus = localStorage.getItem('ControllerApiStatus') || 'unknown';
return (
<div>
<h1>Operator Panel</h1>
<p>API Status: {apiStatus}</p>
<button onClick={toggleHandover}>
{handover ? 'Release to AI' : 'Take Over Conversation'}
</button>
</div>
);
}When the operator activates handover, they can send messages to the active conversation using the Virbe REST API. See Integration Scenarios for the full API-based message sending flow.
Step 4 – Sending Operator Messages via API
Once the operator has taken over, use the Messages API to send messages to the active conversation:
curl -X POST \
"https://YOUR_DASHBOARD_URL/api/v1/conversations/CONVERSATION_ID/messages" \
-H "x-virbe-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": {
"type": "message",
"text": "Hi, this is a human operator. How can I help you?"
}
}'To discover the active conversation ID, poll the List Conversations endpoint by profileId. See Integration Scenarios – Step 2 for the detailed flow.
Multiple Display Modes
The demo repository implements three customer-facing views, each with slightly different handover behavior:
- Web Avatar View – standard widget with a visible avatar. On handover: interrupts speech and stops the conversation.
- Avatarless View – widget without a visible avatar, used as a secondary touch control. On handover: stops the conversation and manages audio routing between direct playback and WebRTC.
- Kiosk Stream View – pixel-streamed video of a virtual being on a display screen. On handover: disconnects the stream and shows a handover message.
All three views read the same HumanHandover localStorage flag and react to changes in real time.
Full Source Code
The complete working example with all views, the operator panel, and routing setup is available on GitHub:
VirbeHQ/virbe-react-integration-demo – human-handover
To run the demo locally:
git clone https://github.com/VirbeHQ/virbe-react-integration-demo.git
cd virbe-react-integration-demo/human-handover
cp .env.sample .env # Fill in your profile ID, secret, and dashboard URL
npm install
npm run devThen open the following routes in separate browser windows:
/display-web-widget– customer-facing widget with avatar/touch– avatarless touch control/admin– operator panel