Skip to main content

IVR JS Example (Hotel Reception)

IVR JS Example - Hotel Reception

Demonstrating Vodia's Javascript IVR capabilities, this example script includes TTS output, OpenAI integration to provide hotel reception functionality, and Caller ID handling where collected caller ID and guest details are sent to an external application for further processing(for e.g. Hotel's Booking system).

Setup:

  • Create a new project in OpenAI platform for your SIP connection to OpenAI.
  • Get the project id, and the related API key, to be used below.
  • Setup a webhook in this project - a URL that can reach your Vodia PBX - with the endpoint e.g.: https://YOUR-VODIA-TENANT.com/openai. The "/openai" is the default, but you can change it here and the PBX.
  • Create a new "OpenAI" trunk in Vodia PBX for connection to OpenAI. Make sure to enter the project id (taken above) as the username when creating the trunk.
  • Create a dialplan entry for use of that trunk with the Pattern as openai and replacement as empty.
  • Now you can use an IVR to use JS and the API key to connect (WS) to OpenAI for setting up instructions and receive responses as setup by the instruction like function calls for call routing etc.
  • One example is given below.

Scenario:

  • TTS Output: Delivering a welcome message via Vodia's Text-to-Speech.
  • OpenAI Integration: Providing hotel reception functionality.
  • Guest Information Collection: Gathering details such as name, country, number of guests, and check-in/check-out dates.
  • External Application Communication: Transmitting the Caller ID and collected guest details in JSON format to an external application upon detection of relevant information in the transcript. This could be the Hotel's Booking system
  • Intelligent Call Routing: Transferring the call to the appropriate department (e.g., Room Information, Reservations, Restaurant).
tip

Please customize these example instruction prompts for OpenAI to align with your specific needs.

note

The model specified in the script is gpt-4o-realtime-preview-2025-06-03. You must verify its current availability under your OpenAI plan. Additionally, check for potential rate limit/cost impacts and identify a suitable alternative model should the preview version be inaccessible

//
// OpenAI integration for Vodia Hotel
//
// (C) Vodia Networks 2025
//
// This file is property of Vodia Networks Inc. All rights reserved.
// For more information mail Vodia Networks Inc., info@vodia.com.
//
'use strict';

// API key for OpenAI
var secret = "sk-proj-KEY";

// Get caller number and IVR called
var from = tables['cobjs'].get(call.callid, 'from');
var to = tables['cobjs'].get(call.callid, 'to');

// Status flags
var transferComplete = false;
var responseComplete = false;

// Storage for transfer information
var pendingTransfer = null;
var pendingStartDate = null;
var pendingLastDate = null;
// New guest information fields
var pendingGuestName = null;
var pendingGuestCountry = null;
var pendingGuestCount = null;

// Set timeout for the call (5 minutes)
var timer = setTimeout(function() {
if (!transferComplete) {
console.log("Call timeout reached (5 minutes), transferring to default extension 700");
call.transfer('700');
}
}, 300000);

// Function to send booking date information and guest details to webhook
function send(start_date, last_date, guest_name, guest_country, guest_count, from, to) {
console.log("WEBHOOK: Preparing to send reservation data");

var body = JSON.stringify({
startdate: start_date,
enddate: last_date,
name: guest_name,
country: guest_country,
guests: guest_count,
callernumber: from,
callednumber: to
});
console.log("WEBHOOK: Request body: " + body);

var args = {
method: 'POST',
url: 'https://external_web_server/webhook',
header: [ { name: 'Content-Type', value: 'application/json' } ]
};

if (body) {
args.body = body;
}

console.log("WEBHOOK: Sending HTTP request to webhook");
try {
system.http(args);
console.log("WEBHOOK: HTTP request sent successfully");
} catch (e) {
console.log("WEBHOOK ERROR: " + e.message);
}
}

// Helper function to execute transfer with proper logging
function executeTransfer() {
if (transferComplete) return;

// Add date information to the log if available
if (pendingStartDate) console.log("Booking start_date: " + pendingStartDate);
if (pendingLastDate) console.log("Booking last_date: " + pendingLastDate);
if (pendingGuestName) console.log("Guest name: " + pendingGuestName);
if (pendingGuestCountry) console.log("Guest country: " + pendingGuestCountry);
if (pendingGuestCount) console.log("Number of guests: " + pendingGuestCount);

// Send booking information to webhook if we have any reservation data
if (pendingTransfer == "701" || // Only for reservations department transfers
pendingStartDate || pendingLastDate ||
pendingGuestName || pendingGuestCountry || pendingGuestCount) {

console.log("Sending reservation data to webhook");
// If data is missing, use defaults or empty values
var startDate = pendingStartDate || "01/01/2025";
var endDate = pendingLastDate || "02/01/2025";
var guestName = pendingGuestName || "Unknown";
var guestCountry = pendingGuestCountry || "Unknown";
var guestCount = pendingGuestCount || "1";

send(startDate, endDate, guestName, guestCountry, guestCount, from, to);
}

// Mark transfer as complete to prevent multiple transfers
transferComplete = true;

// Clear the timeout as we're handling the transfer now
if (timer) clearTimeout(timer);

// Add a small final delay to ensure any in-progress audio completes
setTimeout(function() {
// Execute the transfer
call.mute();
try {
console.log("Transferring call to extension: " + pendingTransfer);
call.transfer(pendingTransfer);
console.log("Transfer command executed");
} catch (e) {
console.log("Transfer error: " + e.message);
}
}, 500);
}

// HTTP callback handler for OpenAI Realtime API
function onhttp(args) {
console.log('OpenAI Realtime API incoming call event');
console.log(JSON.stringify(args));

const body = JSON.parse(args.body);

if (body.type == 'realtime.call.incoming') {
const callid = body.data.call_id;
console.log('Received call_id: ' + callid);

// Accept the 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"
}),
callback: function(code, response, headers) {
console.log('Call accepted, code: ' + code);
initializeOpenAI(callid);
}
});
}
}

// Initialize OpenAI WebSocket connection
function initializeOpenAI(callid) {
console.log('Initializing OpenAI WebSocket 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/Version" }
]);

ws.on('open', function() {
console.log("OpenAI websocket opened");

// Send session update with instructions
const update = {
"type": "session.update",
"session": {
"type": "realtime",
"instructions": "You are Scott, a refined hotel concierge at Vodia Hotel. Detect the caller's language and respond accordingly. For English callers, use a sophisticated male English accent. Your task is to assist guests with inquiries and collect reservation details when needed. When transferring calls or collecting guest information, you MUST include this data in your transcript in this exact format: 'transfer: [extension]', 'guest_name: [name]', 'guest_country: [country]', 'guest_count: [number]', 'start_date: DD/MM/YYYY', 'last_date: DD/MM/YYYY'. Extensions: Reservations=701, Concierge=702, Housekeeping=703, Room Service=704, Manager=705, Default=700. Be professional, warm, and helpful.",
"voice": "echo",
"temperature": 0.7,
"turn_detection": {
"type": "server_vad",
"threshold": 0.5,
"prefix_padding_ms": 300,
"silence_duration_ms": 500
}
}
};

ws.send(JSON.stringify(update));

// Send initial greeting
const greeting = {
"type": "response.create",
"response": {
"instructions": "Greet the caller immediately. Detect their language and respond accordingly. If English, say: 'Thank you for calling Vodia Hotel. This is Scott speaking. How may I assist you today?' Use a refined, male English accent. For other languages, provide an equivalent greeting while maintaining a professional, sophisticated demeanor."
}
};

ws.send(JSON.stringify(greeting));
});

ws.on('close', function() {
console.log("OpenAI websocket closed");
});

ws.on('message', function(message) {
const msg = JSON.parse(message);

// Log all non-audio messages
if (msg.type !== "response.audio.delta") {
console.log('WebSocket message: ' + JSON.stringify(msg));
}

// Handle transcript completion
if (msg.type == "response.audio_transcript.done") {
if (msg.transcript) {
console.log("Message transcript: " + msg.transcript);

// Extract all information from transcript
// Extract date information if present - using regex patterns
if (msg.transcript.indexOf("start_date:") !== -1) {
var startDateMatch = msg.transcript.match(/start_date:\s*(\d{1,2}\/\d{1,2}\/\d{4})/i);
if (startDateMatch && startDateMatch[1]) {
var start_date = startDateMatch[1].trim();
console.log("Extracted start_date: " + start_date);
pendingStartDate = start_date;
}
}

if (msg.transcript.indexOf("last_date:") !== -1) {
var lastDateMatch = msg.transcript.match(/last_date:\s*(\d{1,2}\/\d{1,2}\/\d{4})/i);
if (lastDateMatch && lastDateMatch[1]) {
var last_date = lastDateMatch[1].trim();
console.log("Extracted last_date: " + last_date);
pendingLastDate = last_date;
}
}

// Extract guest name
if (msg.transcript.indexOf("guest_name:") !== -1) {
var nameMatch = msg.transcript.match(/guest_name:\s*([^,\.]+)/i);
if (nameMatch && nameMatch[1]) {
var guest_name = nameMatch[1].trim();
console.log("Extracted guest_name: " + guest_name);
pendingGuestName = guest_name;
}
}

// Extract guest country
if (msg.transcript.indexOf("guest_country:") !== -1) {
var countryMatch = msg.transcript.match(/guest_country:\s*([^,\.]+)/i);
if (countryMatch && countryMatch[1]) {
var guest_country = countryMatch[1].trim();
console.log("Extracted guest_country: " + guest_country);
pendingGuestCountry = guest_country;
}
}

// Extract guest count
if (msg.transcript.indexOf("guest_count:") !== -1) {
var countMatch = msg.transcript.match(/guest_count:\s*(\d+)/i);
if (countMatch && countMatch[1]) {
var guest_count = countMatch[1].trim();
console.log("Extracted guest_count: " + guest_count);
pendingGuestCount = guest_count;
}
}

// Check if transcript contains the transfer command
if (!transferComplete && msg.transcript.indexOf("transfer: ") !== -1) {
var parts = msg.transcript.split("transfer: ");
if (parts.length > 1) {
var extension = parts[1].trim();
console.log("Found transfer code: " + extension);

// Store for later execution
pendingTransfer = extension;
}
}
}
}
else if (msg.type == "response.audio.done") {
console.log("Audio playback completed");

// If we have a pending transfer, now it's safe to execute it
if (pendingTransfer && !transferComplete) {
console.log("Audio finished, now safe to execute transfer to extension: " + pendingTransfer);
executeTransfer();
}
}
else if (msg.type == "response.done") {
// Mark that the full response is complete
responseComplete = true;
console.log("Response completed, checking for pending transfer");

// If we have a pending transfer, schedule fallback execution
if (pendingTransfer && !transferComplete) {
console.log("Transfer ready, waiting for audio playback to finish");

// Safety fallback in case audio.done never fires
setTimeout(function() {
if (!transferComplete && pendingTransfer) {
console.log("FALLBACK: Audio.done event may not have fired, executing delayed transfer to extension: " + pendingTransfer);
executeTransfer();
}
}, 15000);
}
}
});

ws.connect();
}

// Start the call flow
console.log("Starting hotel IVR with OpenAI Realtime API");
call.http(onhttp);
call.dial('openai');

The external application will receive the following data. (Example data follows.)

{"startdate": "15/05/2025", "enddate": "30/05/2025", "name": "Christo Da Silva", "country": "Australia", "guests": "5", "callernumber": "test <sip:504@sbc.vodia-teams.com>", "callednumber": "open ai -hotel <sip:101@sbc.vodia-teams.com>"}
tip

Use 'Connection': 'keep-alive' header in your response.

For more information on Vodia's JavaScript capabilities, refer to: Vodia Backend JavaScript Documentation