IVR JS Example (Customer verification)
IVR JS Example - Customer verification
Demonstrating Vodia's Javascript IVR capabilities, this example script showcases advanced call handling techniques for secure banking verification, combining DTMF input, OpenAI voice integration, and external system communication to create a robust customer verification flow.
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:
- Multi-Factor Authentication: Securely collecting credit card numbers and date of birth via DTMF, then personal details via voice.
- OpenAI Integration: Providing natural language conversational capabilities for collecting customer information.
- Personal Information Collection: Gathering details such as first name, last name, and address through conversational AI.
- External System Integration: Transmitting collected customer data in JSON format to an external verification system. The external system uses this data to send an OTP to the customer's mobile phone.
- OTP Verification Flow: The PBX gathers the OTP and transmits it to the external system for verification.
- Intelligent Call Routing: Transferring verified customers to appropriate departments based on verification results.
Please tailor these example OpenAI instruction prompts to meet your specific requirements and ensure all necessary security measures are implemented and tested.
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
Implementation Details
The script follows a clear sequential flow:
- Initial Customer Greeting: TTS welcomes customer and requests credit card information
- DTMF Collection Phase: Collects credit card and DOB through touch-tone input(DTMF)
- OpenAI Conversation Phase: Connects to OpenAI to collect name and address
- Primary Verification: Sends all collected information to external verification system
- OTP Verification (if required): Collects one-time password and send it to the external system for verification.
- Call Completion: Transfers to appropriate department based on verification results
//
// Bank Customer Verification IVR with OpenAI Realtime API
//
// This script handles customer verification through:
// 1. DTMF capture for credit card number and DOB
// 2. Voice interaction for address and name collection
// 3. External validation with OTP verification
//
'use strict';
// Customer information storage
var creditCardNumber = '';
var dateOfBirth = '';
var firstName = '';
var lastName = '';
var address = '';
var otpCode = '';
var to = '';
var from = '';
var cleanFirstName = '';
var cleanLastName = '';
var cleanAddress = '';
// 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 creditCardCollected = false;
var dobCollected = false;
var personalInfoCollected = false;
var verificationSent = false;
var otpRequested = false;
var otpVerified = false;
var transferComplete = false;
// 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 verification data to webhook
function sendVerificationData() {
console.log("WEBHOOK: Preparing to send verification data");
// Clean up data before sending
var cleanFirstName = firstName ? firstName.replace(/last_name.*$/i, '').trim() : '';
var cleanLastName = lastName ? lastName.replace(/address.*$/i, '').trim() : '';
var cleanAddress = address ? address.replace(/verification_complete.*$/i, '').trim() : '';
var body = JSON.stringify({
creditCardNumber: creditCardNumber,
dateOfBirth: dateOfBirth,
firstName: cleanFirstName,
lastName: cleanLastName,
address: cleanAddress,
callernumber: from,
callednumber: to
});
console.log("WEBHOOK: Request body: " + body);
var args = {
method: 'POST',
url: 'http://external_server/webhook',
header: [ { name: 'Content-Type', value: 'application/json' } ],
callback: handleVerificationResponse
};
if (body) {
args.body = body;
}
console.log("WEBHOOK: Sending HTTP request to webhook");
verificationSent = true;
try {
system.http(args);
console.log("WEBHOOK: HTTP request sent successfully");
} catch (e) {
console.log("WEBHOOK ERROR: " + e.message);
handleFailure();
}
}
// Function to send OTP verification to webhook
function sendOtpVerification() {
console.log("WEBHOOK: Preparing to send OTP verification");
var body = JSON.stringify({
creditCardNumber: creditCardNumber,
otpCode: otpCode,
callernumber: from,
callednumber: to
});
console.log("WEBHOOK: Request body: " + body);
var args = {
method: 'POST',
url: 'http://external_server/verifyotp',
header: [ { name: 'Content-Type', value: 'application/json' } ],
callback: handleOtpResponse
};
if (body) {
args.body = body;
}
console.log("WEBHOOK: Sending OTP verification request");
try {
system.http(args);
console.log("WEBHOOK: OTP verification request sent successfully");
} catch (e) {
console.log("WEBHOOK ERROR: " + e.message);
handleFailure();
}
}
// Callback function to handle verification response
function handleVerificationResponse(code, response, headers) {
console.log("Verification response code: " + code);
console.log("Verification response: " + response.toString());
try {
var responseData = JSON.parse(response);
// Play message if provided
if (responseData.message) {
call.say(responseData.message);
console.log("Playing message: " + responseData.message);
}
// Handle various verification outcomes
if (responseData.otp_required) {
// Request OTP from customer
otpRequested = true;
setTimeout(function() {
call.say("Please enter your 6-digit verification PIN.");
otpCode = '';
call.dtmf(onOtpDtmf);
}, 1000);
} else if (responseData.transfer) {
// Handle direct transfer
var destination = responseData.destination || "700";
setTimeout(function() {
executeTransfer(destination);
}, 3000);
} else {
// Default fallback
handleFailure();
}
} catch (e) {
console.log("Error parsing verification response: " + e.message);
handleFailure();
}
}
// Callback function to handle OTP verification response
function handleOtpResponse(code, response, headers) {
console.log("OTP verification response code: " + code);
console.log("OTP verification response: " + response.toString());
try {
var responseData = JSON.parse(response);
// Play message if provided
if (responseData.message) {
call.say(responseData.message);
console.log("Playing message: " + responseData.message);
}
// Handle OTP verification result
if (responseData.otp_verified) {
otpVerified = true;
setTimeout(function() {
call.say("Your identity has been successfully verified. Thank you for using our service.");
executeTransfer("700");
}, 3000);
} else if (responseData.retry_otp) {
// Allow customer to retry OTP
setTimeout(function() {
call.say("The verification code is incorrect. Please enter your 6-digit verification PIN again.");
otpCode = '';
call.dtmf(onOtpDtmf);
}, 2000);
} else if (responseData.transfer) {
// Handle direct transfer
var destination = responseData.destination || "705";
setTimeout(function() {
executeTransfer(destination);
}, 3000);
} else {
// Default fallback for failed verification
setTimeout(function() {
call.say("Verification failed. Transferring you to a customer service representative.");
executeTransfer("705");
}, 2000);
}
} catch (e) {
console.log("Error parsing OTP verification response: " + e.message);
handleFailure();
}
}
// Helper function for failures
function handleFailure() {
setTimeout(function() {
call.say("We're experiencing technical difficulties. Transferring you to customer service.");
executeTransfer("705");
}, 2000);
}
// Function to execute transfer
function executeTransfer(destination) {
if (transferComplete) return;
// Log all collected information
console.log("Credit Card Number: " + creditCardNumber);
console.log("Date of Birth: " + dateOfBirth);
console.log("Name: " + firstName + " " + lastName);
console.log("Address: " + address);
if (otpCode) console.log("OTP Code: " + otpCode);
console.log("OTP Verified: " + otpVerified);
// 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 delay to ensure any in-progress audio completes
setTimeout(function() {
// Execute the transfer
call.mute();
try {
console.log("Transferring call to extension: " + destination);
call.transfer(destination);
console.log("Transfer command executed");
} catch (e) {
console.log("Transfer error: " + e.message);
}
}, 500);
}
// DTMF handler for credit card number
function onCreditCardDtmf(digit) {
creditCardNumber += digit;
console.log("Credit card input: " + creditCardNumber.length + " digits");
// Assuming credit card is 16 digits
if (creditCardNumber.length === 16) {
console.log("Credit card collection complete: " + creditCardNumber);
creditCardCollected = true;
call.say("Thank you. Now, please enter your date of birth in format DD MM YYYY For example, 12031994 for 12th March 1994.");
dateOfBirth = '';
call.dtmf(onDobDtmf);
}
}
// DTMF handler for date of birth
function onDobDtmf(digit) {
dateOfBirth += digit;
console.log("Date of birth input: " + dateOfBirth.length + " digits");
// Date of birth should be 8 digits (DDMMYYYY)
if (dateOfBirth.length === 8) {
console.log("Date of birth collection complete: " + dateOfBirth);
dobCollected = true;
// Now initiate OpenAI Realtime API
call.http(onhttp);
call.dial('openai');
}
}
// DTMF handler for OTP code
function onOtpDtmf(digit) {
otpCode += digit;
console.log("OTP input: " + otpCode.length + " digits");
// OTP should be 6 digits
if (otpCode.length === 6) {
console.log("OTP collection complete: " + otpCode);
sendOtpVerification();
}
}
// 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 a bank verification assistant. Your job is to politely collect the customer's first name, last name, and complete address. The customer has already provided their credit card number and date of birth. Ask for first name, then last name, then complete address - one at a time. Confirm each answer before moving on. IMPORTANT: At the end of conversation, you must output exactly in this format: 'first_name: [FirstName]', 'last_name: [LastName]', 'address: [FullAddress]', and 'verification_complete: true'. Keep responses concise and professional.",
"voice": "alloy",
"temperature": 0.7,
"turn_detection": {
"type": "server_vad",
"threshold": 0.3,
"prefix_padding_ms": 500,
"silence_duration_ms": 2500
}
}
};
ws.send(JSON.stringify(update));
// Send initial greeting
const greeting = {
"type": "response.create",
"response": {
"instructions": "Greet the customer with: 'Thank you for providing your information. For verification purposes, may I please have your first name?'"
}
};
ws.send(JSON.stringify(greeting));
});
ws.on('close', function() {
console.log("OpenAI websocket closed");
// If we have all the necessary information, send it for verification
if (firstName && lastName && address && !verificationSent) {
personalInfoCollected = true;
sendVerificationData();
} else if (!personalInfoCollected) {
// Fallback if we didn't collect all information
handleFailure();
}
});
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));
}
// Extract transcript information
if (msg.type == "response.audio_transcript.done" && msg.transcript) {
console.log("Message transcript: " + msg.transcript);
// Extract information from transcript
if (msg.transcript.indexOf("first_name:") !== -1) {
var nameMatch = msg.transcript.match(/first_name:\s*([^\n,]+)/i);
if (nameMatch && nameMatch[1]) {
firstName = nameMatch[1].trim();
console.log("Extracted first name: " + firstName);
}
}
if (msg.transcript.indexOf("last_name:") !== -1) {
var lastNameMatch = msg.transcript.match(/last_name:\s*([^\n,]+)/i);
if (lastNameMatch && lastNameMatch[1]) {
lastName = lastNameMatch[1].trim();
console.log("Extracted last name: " + lastName);
}
}
if (msg.transcript.indexOf("address:") !== -1) {
var addressMatch = msg.transcript.match(/address:\s*([^\n]+(?:\n[^v][^\n]*)*)/i);
if (addressMatch && addressMatch[1]) {
address = addressMatch[1].trim();
console.log("Extracted address: " + address);
}
}
// Check if verification is complete
if (msg.transcript.indexOf("verification_complete: true") !== -1) {
console.log("Verification information collection complete");
personalInfoCollected = true;
// Check if we have all required information
if (firstName && lastName && address) {
console.log("All required information collected, closing WebSocket");
// Close the websocket which will trigger verification
setTimeout(function() {
ws.close();
}, 2000);
}
}
}
});
ws.connect();
}
// Start the call flow
console.log("Starting bank verification IVR");
call.say('Welcome to our secure banking service. For verification purposes, please enter your 16-digit credit card number.');
creditCardNumber = '';
call.dtmf(onCreditCardDtmf);