Voice Agents JS Example (Hotel Reception)
Voice Agents JS Example - Hotel Reception
Demonstrating Vodia's Javascript Voice Agents capabilities, this example script uses OpenAI's Realtime API with SIP integration to provide sophisticated hotel reception functionality. The system collects guest information through natural conversation, sends data to external booking systems, and intelligently routes calls based on webhook responses.
OpenAI Setup:
Please refer to the documentation here for OpenAI setup, including configuring project, the webhook, API key and then setting up the trunk and dialplan on Vodia.
Scenario:
- SIP-Based Integration: Uses OpenAI's Realtime API via SIP for reliable call handling
- Function-Based Data Collection: Collects guest information using structured OpenAI function calls
- Comprehensive Guest Information: Gathers name, country, number of guests, room type preference (double/king/family suite), and check-in/check-out dates
- External System Integration: Sends collected data in JSON format to external booking systems via webhook
- Dynamic Call Routing: Processes webhook responses to transfer calls based on booking system instructions
- Intelligent Conversation Flow: Natural dialogue that asks one question at a time with proper acknowledgment
- Multi-Department Support: Routes calls to appropriate departments (Reservations, Room Information, Restaurant, etc.)
Please customize the AI instructions and department extensions to align with your specific needs.
The model specified uses OpenAI's gpt-realtime model. You must verify its current availability under your OpenAI plan. Additionally, check for potential rate limit/cost impacts.
//
// OpenAI Hotel Reception - SIP Style Integration
// (C) Vodia Networks 2026
//
'use strict';
var secret = "sk-proj-YOUR_API_KEY_HERE";
// Get caller information
var from = tables['cobjs'].get(call.callid, 'from');
var to = tables['cobjs'].get(call.callid, 'to');
// Storage for collected information
var guestData = {
name: null,
country: null,
count: null,
startDate: null,
endDate: null,
roomType: null
};
// Set timeout for the call (5 minutes)
var timer = setTimeout(function() {
console.log("Call timeout reached (5 minutes), transferring to default extension 700");
call.transfer('700');
}, 300000);
// Function to send booking data to webhook
function sendWebhook(data, callback) {
console.log("WEBHOOK: Preparing to send reservation data");
var body = JSON.stringify({
startdate: data.startDate || "01/01/2025",
enddate: data.endDate || "02/01/2025",
name: data.name || "Unknown",
country: data.country || "Unknown",
guests: data.count || "1",
roomtype: data.roomType || "double",
callernumber: from,
callednumber: to
});
console.log("WEBHOOK: Request body: " + body);
var args = {
method: 'POST',
url: 'https://external-server/webhook',
header: [{ name: 'Content-Type', value: 'application/json' }],
body: body,
callback: function(code, response, headers) {
console.log("WEBHOOK: Response code: " + code);
console.log("WEBHOOK: Response body: " + response);
if (callback) callback(code, response);
}
};
console.log("WEBHOOK: Sending HTTP request");
try {
system.http(args);
} catch (e) {
console.log("WEBHOOK ERROR: " + e.message);
if (callback) callback(0, null);
}
}
// HTTP callback handler
call.http(onhttp);
function onhttp(args) {
console.log('OpenAI Event Received');
console.log(JSON.stringify(args));
const body = JSON.parse(args.body);
console.log('Body: ' + JSON.stringify(body));
if (body.type == 'realtime.call.incoming') {
const callid = body.data.call_id;
console.log('Call ID: ' + callid);
// Accept the realtime call
system.http({
method: 'POST',
url: 'https://api.openai.com/v1/realtime/calls/' + callid + '/accept',
header: [
{ name: 'Authorization', value: 'Bearer ' + secret, secret: true },
{ name: 'Content-Type', value: 'application/json' }
],
body: JSON.stringify({
type: "realtime",
model: "gpt-realtime",
instructions: "You are Scott, a sophisticated, professional male receptionist at Vodia Hotel with an English accent."
}),
callback: function(code, response, headers) {
connected(code, response, headers, callid);
}
});
}
}
function connected(code, response, headers, callid) {
console.log('WebSocket connecting with call_id: ' + callid);
var ws = new Websocket("wss://api.openai.com/v1/realtime?call_id=" + callid);
ws.header([
{ name: "Authorization", value: "Bearer " + secret, secret: true },
{ name: "User-Agent", value: "Vodia-PBX/70.0" }
]);
ws.on('open', function() {
console.log("WebSocket opened");
const update = {
"type": "session.update",
"session": {
"type": "realtime",
"instructions": "You are Scott, a sophisticated, professional male receptionist at Vodia Hotel with an English accent. Your job is to greet callers with refined warmth, identify their needs through thoughtful questions, and direct them to the appropriate department. NEVER announce system variables to the caller. DO NOT summarize at the end of the call. Always ask 1 question at a time. Sound like a hotel receptionist. Use words like check-in date and check-out date. MULTILINGUAL CAPABILITY: Automatically detect the caller's language and respond in that same language. You are fluent in all languages. When speaking languages other than English, maintain the same professional, sophisticated demeanor that befits a high-end hotel receptionist. VOICE & TONE: Always speak in a polished, articulate manner befitting a male English receptionist. Express elegant courtesy and subtle empathy. Use sophisticated yet accessible language as a well-trained hotel receptionist would. Keep responses concise but impeccably helpful. PROCESS: 1. Listen carefully to caller's initial description. 2. Ask clarifying questions if needed to understand their request. 3. Determine which department they need. 4. CRITICAL FOR RESERVATIONS: If this is a reservation inquiry, you MUST collect ALL of the following information by calling the functions in this order before transferring: a) Guest name - call 'set_guest_name' function b) Country of origin - call 'set_guest_country' function c) Number of guests - call 'set_guest_count' function d) Room type preference - ALWAYS ask 'Would you prefer a double room, king room, or family suite?' and call 'set_room_type' function e) Check-in and check-out dates - call 'set_booking_dates' function with DD/MM/YYYY format. DO NOT call 'transfer_call' until you have called ALL FIVE functions above for reservations. 5. For NON-RESERVATION inquiries, you can transfer immediately. 6. After collecting ALL required information, tell the caller you'll connect them and call the 'transfer_call' function. DEPARTMENT CODES: For RESERVATIONS or booking modifications: 701. For ROOM INFORMATION and current rates: 702. For HOTEL AMENITIES and services: 703. For DIRECTIONS to the property: 704. For RESTAURANT or room service: 705. For FRONT DESK or concierge: 706. For ALL OTHER inquiries: 700. Do not respond with normal text for transfer intents - always use the functions.",
"tools": [
{
"type": "function",
"name": "set_guest_name",
"description": "Records the guest's name for the reservation",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Guest's full name"
}
},
"required": ["name"]
}
},
{
"type": "function",
"name": "set_guest_country",
"description": "Records the guest's country of origin",
"parameters": {
"type": "object",
"properties": {
"country": {
"type": "string",
"description": "Guest's country"
}
},
"required": ["country"]
}
},
{
"type": "function",
"name": "set_guest_count",
"description": "Records the number of guests staying",
"parameters": {
"type": "object",
"properties": {
"count": {
"type": "string",
"description": "Number of guests"
}
},
"required": ["count"]
}
},
{
"type": "function",
"name": "set_room_type",
"description": "Records the preferred room type for the reservation",
"parameters": {
"type": "object",
"properties": {
"roomtype": {
"type": "string",
"description": "Room type: double, king, or family suite",
"enum": ["double", "king", "family suite"]
}
},
"required": ["roomtype"]
}
},
{
"type": "function",
"name": "set_booking_dates",
"description": "Records the check-in and check-out dates for the reservation",
"parameters": {
"type": "object",
"properties": {
"checkin": {
"type": "string",
"description": "Check-in date in DD/MM/YYYY format"
},
"checkout": {
"type": "string",
"description": "Check-out date in DD/MM/YYYY format"
}
},
"required": ["checkin", "checkout"]
}
},
{
"type": "function",
"name": "transfer_call",
"description": "Transfers the call to the appropriate hotel department",
"parameters": {
"type": "object",
"properties": {
"extension": {
"type": "string",
"description": "Department extension number (700-706)"
}
},
"required": ["extension"]
}
}
],
"tool_choice": "auto"
}
};
ws.send(JSON.stringify(update));
// Send initial greeting
const greeting = {
"type": "response.create",
"response": {
"instructions": "Greet with: Thank you for calling Vodia Hotel. This is Scott speaking. How may I assist you today?"
}
};
ws.send(JSON.stringify(greeting));
});
ws.on('close', function() {
console.log("WebSocket closed");
});
ws.on('message', function(message) {
const evt = JSON.parse(message);
if (evt.type === "response.output_item.done" && evt.item.type === "function_call") {
const args = JSON.parse(evt.item.arguments);
const functionName = evt.item.name;
const callId = evt.item.call_id;
console.log('Function called: ' + functionName);
console.log('Arguments: ' + JSON.stringify(args));
var result = {};
if (functionName === "set_guest_name") {
guestData.name = args.name;
console.log('Guest name set: ' + guestData.name);
result = { success: true, message: "Name recorded" };
}
else if (functionName === "set_guest_country") {
guestData.country = args.country;
console.log('Guest country set: ' + guestData.country);
result = { success: true, message: "Country recorded" };
}
else if (functionName === "set_guest_count") {
guestData.count = args.count;
console.log('Guest count set: ' + guestData.count);
result = { success: true, message: "Guest count recorded" };
}
else if (functionName === "set_room_type") {
guestData.roomType = args.roomtype;
console.log('Room type set: ' + guestData.roomType);
result = { success: true, message: "Room type recorded" };
}
else if (functionName === "set_booking_dates") {
guestData.startDate = args.checkin;
guestData.endDate = args.checkout;
console.log('Booking dates set: ' + guestData.startDate + ' to ' + guestData.endDate);
result = { success: true, message: "Dates recorded" };
}
else if (functionName === "transfer_call") {
const extension = args.extension;
console.log('Transfer requested to extension: ' + extension);
// Clear timeout
if (timer) clearTimeout(timer);
// Send webhook if we have reservation data (extension 701 or any collected data)
if (extension == "701" || guestData.name || guestData.country ||
guestData.count || guestData.startDate || guestData.endDate || guestData.roomType) {
console.log('Sending webhook and waiting for response...');
sendWebhook(guestData, function(code, response) {
if (code == 200 && response) {
try {
var res = JSON.parse(response);
console.log('Webhook response: ' + JSON.stringify(res));
// If there's a transfer request, process it
if (res.transfer && res.destination) {
console.log('Webhook instructed transfer to: ' + res.destination);
// Say the message from webhook
if (res.message) {
call.say({
text: res.message,
callback: function() {
console.log('Message played, transferring to: ' + res.destination);
call.transfer(res.destination);
}
});
} else {
// No message, transfer immediately
setTimeout(function() {
call.transfer(res.destination);
}, 1000);
}
} else {
// Fallback to original extension
console.log('No transfer in webhook response, using original extension: ' + extension);
setTimeout(function() {
call.transfer(extension);
}, 1000);
}
} catch (e) {
console.log('Error parsing webhook response: ' + e.message);
// Fallback to original extension
setTimeout(function() {
call.transfer(extension);
}, 1000);
}
} else {
console.log('Webhook failed, using original extension: ' + extension);
// Fallback to original extension
setTimeout(function() {
call.transfer(extension);
}, 1000);
}
});
result = { success: true, message: "Processing your request..." };
} else {
// No reservation data, just transfer
result = { success: true, message: "Transferring to extension " + extension };
setTimeout(function() {
console.log('Executing transfer to: ' + extension);
call.transfer(extension);
}, 1000);
}
}
// Send function result back to OpenAI to continue conversation
console.log('Sending function result for: ' + functionName);
const functionResult = {
"type": "conversation.item.create",
"item": {
"type": "function_call_output",
"call_id": callId,
"output": JSON.stringify(result)
}
};
ws.send(JSON.stringify(functionResult));
// Request next response to continue conversation
setTimeout(function() {
const responseRequest = {
"type": "response.create"
};
ws.send(JSON.stringify(responseRequest));
}, 100);
}
});
ws.connect();
}
// Initiate the call to OpenAI
call.dial('openai');
Webhook Request Format
The external booking system will receive guest data in the following JSON format:
{
"startdate": "28/01/2026",
"enddate": "30/01/2026",
"name": "John Doe",
"country": "Australia",
"guests": "2",
"roomtype": "king",
"callernumber": "\"Customer\" <sip:+61433337285@phones.pbx70.vodia-teams.com>",
"callednumber": "\"Hotel Reception\" <sip:61272010747@phones.pbx70.vodia-teams.com>"
}
Webhook Response Format
Your external booking system can control the call flow by responding with:
{
"destination": "501",
"message": "We have successfully received your booking details. I'll now transfer you to confirm your reservation.",
"transfer": "true"
}
Response Fields:
destination: Extension number to transfer the call tomessage: Text that will be spoken to the caller before transfer (optional)transfer: Set to"true"to enable webhook-controlled routing
- Use
'Connection': 'keep-alive'header in your webhook responses for optimal performance - If the webhook doesn't respond or returns an error, the system will fallback to the original department extension
- The
messagefield supports natural language and will be spoken using Vodia's TTS engine
Key Features
Function-Based Collection: Uses OpenAI's function calling for reliable, structured data extraction rather than parsing transcripts
Progressive Data Gathering: Asks one question at a time and acknowledges each response before proceeding
Webhook Integration: Sends collected data to external systems and processes their routing instructions
Fallback Handling: Automatically falls back to default routing if webhook fails
Multilingual Support: Automatically detects caller's language and responds appropriately
Timeout Protection: 5-minute call timeout with automatic transfer to default extension
For more information on Vodia's JavaScript capabilities, refer to: Vodia Backend JavaScript Documentation