Private Voice Communication
This use case shows you how to implement the idea described in Private Voice Communication use case. It teaches you how to build a voice proxy using Vonage's Node Server SDK, using virtual numbers to hide the real phone numbers of the participants. Full source code is also available in our GitHub repo.
Overview
Sometimes you want two users to be able to call each other without revealing their private phone numbers.
For example, if you are operating a ride sharing service, then you want your users to be able to speak to each other to coordinate pick-up times and locations. But you don't want to give out your customers' phone numbers - after all, you have an obligation to protect their privacy. And you don't want them to be able to arrange ride shares directly without using your service because that means lost revenue for your business.
Using Vonage's APIs, you can provide each participant in a call with a temporary number that masks their real number. Each caller sees only the temporary number for the duration of the call. When there is no further need for them to communicate, the temporary number is revoked.
You can download the source code from our GitHub repo.
Prerequisites
In order to work through this use case you need:
- A Vonage account
- The Vonage CLI installed and configured
Code repository
There is a GitHub repository containing the code.
Steps
To build the application, you perform the following steps:
- Overview
- Prerequisites
- Code repository
- Steps
- Configuration
- Create a Voice API application
- Create the web application
- Provision virtual numbers
- Create a call
- Handle inbound calls
- Reverse map real phone numbers to virtual numbers
- Proxy the call
- Conclusion
- Further information
Configuration
You need to create a .env
file containing configuration. Instructions on how to do that are explained in the GitHub README. As you work through this use case you can populate your configuration file with the required values for variables such as API key, API secret, Application ID, debug mode, and provisioned numbers.
Create a Voice API application
A Voice API Application is a Vonage construct and should not be confused with the application you are going to write. Instead, it's a "container" for the authentication and configuration settings you need to work with the API.
You can create a Voice API Application with the Vonage CLI. You must provide a name for the application and the URLs of two webhook endpoints: the first is the one that Vonage's APIs will make a request to when you receive an inbound call on your virtual number and the second is where the API can post event data.
Replace the domain name in the following Vonage CLI command with your ngrok domain name (How to run ngrok) and run it in your project's root directory:
vonage apps:create "Voice Proxy" --voice_answer_url=https://example.com/proxy-call --voice_event_url=https://example.com/event
This command creates a file called voice_proxy.key
that contains authentication information and returns a unique application ID. Make a note of this ID because you'll need it in subsequent steps.
Create the web application
This application uses the Express framework for routing and the Vonage Node Server SDK for working with the Voice API. dotenv
is used so that the application can be configured using a .env
text file.
In server.js
the code initializes the application's dependencies and starts the web server. A route handler is implemented for the application's home page (/
) so that you can test that the server is running by running node server.js
and visiting http://localhost:3000
in your browser:
"use strict";
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.set('port', (process.env.PORT || 3000));
app.use(bodyParser.urlencoded({ extended: false }));
const config = require(__dirname + '/../config');
const VoiceProxy = require('./VoiceProxy');
const voiceProxy = new VoiceProxy(config);
app.listen(app.get('port'), function() {
console.log('Voice Proxy App listening on port', app.get('port'));
});
Note that the code instantiates an object of the VoiceProxy
class to handle the routing of messages sent to your virtual number to the intended recipient's real number. The proxying process is described in proxy the call, but for now be aware that this class initializes the Vonage Server SDK using the API key and secret that you configure in the next step. This enables your application to make and receive voice calls:
const VoiceProxy = function(config) {
this.config = config;
this.nexmo = new Nexmo({
apiKey: this.config.VONAGE_API_KEY,
apiSecret: this.config.VONAGE_API_SECRET
},{
debug: this.config.VONAGE_DEBUG
});
// Virtual Numbers to be assigned to UserA and UserB
this.provisionedNumbers = [].concat(this.config.PROVISIONED_NUMBERS);
// In progress conversations
this.conversations = [];
};
Provision virtual numbers
Virtual numbers are used to hide real phone numbers from your application users.
The following workflow diagram shows the process for provisioning and configuring a virtual number:
To provision a virtual number you search through the available numbers that meet your criteria. For example, a phone number in a specific country with voice capability:
const Nexmo = require('nexmo');
/**
* Create a new VoiceProxy
*/
const VoiceProxy = function(config) {
this.config = config;
this.nexmo = new Nexmo({
apiKey: this.config.NEXMO_API_KEY,
apiSecret: this.config.NEXMO_API_SECRET
},{
debug: this.config.NEXMO_DEBUG
});
// Virtual Numbers to be assigned to UserA and UserB
this.provisionedNumbers = [].concat(this.config.PROVISIONED_NUMBERS);
// In progress conversations
this.conversations = [];
};
/**
* Provision two virtual numbers. Would provision more in a real app.
*/
VoiceProxy.prototype.provisionVirtualNumbers = function() {
// Buy a UK number with VOICE capabilities.
// For this example we'll also get SMS so we can send them a text notification
this.nexmo.number.search('GB', {features: 'VOICE,SMS'}, function(err, res) {
if(err) {
console.error(err);
}
else {
const numbers = res.numbers;
// For demo purposes:
// - Assume that at least two numbers will be available
// - Rent just two virtual numbers: one for each conversation participant
this.rentNumber(numbers[0]);
this.rentNumber(numbers[1]);
}
}.bind(this));
};
Then rent the numbers you want and associate them with your application.
NOTE: Some types numbers require you have a postal address in order to rent them. If you are not able to obtain a number programmatically, visit the Dashboard where you can rent numbers as required.
When any event occurs relating to each number associated with an application, Vonage sends a request to your webhook endpoint with information about the event. After configuration you store the phone number for later use:
/**
* Rent the given numbers
*/
VoiceProxy.prototype.rentNumber = function(number) {
this.nexmo.number.buy(number.country, number.msisdn, function(err, res) {
if(err) {
console.error(err);
}
else {
this.configureNumber(number);
}
}.bind(this));
};
/**
* Configure the number to be associated with the Voice Proxy application.
*/
VoiceProxy.prototype.configureNumber = function(number) {
const options = {
voiceCallbackType: 'app',
voiceCallbackValue: this.config.NEXMO_APP_ID,
};
this.nexmo.number.update(number.country, number.msisdn, options, function(err, res) {
if(err) {
console.error(err);
}
else {
this.provisionedNumbers.push(number);
}
}.bind(this));
};
To provision virtual numbers, visit http://localhost:3000/numbers/provision
in your browser.
You now have the virtual numbers you need to mask communication between your users.
NOTE: In a production application you choose from a pool of virtual numbers. However, you should keep this functionality in place to rent additional numbers on the fly.
Create a call
The workflow to create a call is:
for Each Participant App->>Vonage: SMS to UserA Vonage->>UserA: SMS App->>Vonage: SMS to UserB Vonage->>UserB: SMS
The following call:
/**
* Create a new tracked conversation so there is a real/virtual mapping of numbers.
*/
VoiceProxy.prototype.createConversation = function(userANumber, userBNumber, cb) {
this.checkNumbers(userANumber, userBNumber)
.then(this.saveConversation.bind(this))
.then(this.sendSMS.bind(this))
.then(function(conversation) {
cb(null, conversation);
})
.catch(function(err) {
cb(err);
});
};
Validate the phone numbers
When your application users supply their phone numbers use Number Insight to ensure that they are valid. You can also see which country the phone numbers are registered in:
/**
* Ensure the given numbers are valid and which country they are associated with.
*/
VoiceProxy.prototype.checkNumbers = function(userANumber, userBNumber) {
const niGetPromise = (number) => new Promise ((resolve) => {
this.nexmo.numberInsight.get(number, (error, result) => {
if(error) {
console.error('error',error);
}
else {
return resolve(result);
}
})
});
const userAGet = niGetPromise({level: 'basic', number: userANumber});
const userBGet = niGetPromise({level: 'basic', number: userBNumber});
return Promise.all([userAGet, userBGet]);
};
Map phone numbers to real numbers
Once you are sure that the phone numbers are valid, map each real number to a virtual number and save the call:
/**
* Store the conversation information.
*/
VoiceProxy.prototype.saveConversation = function(results) {
let userAResult = results[0];
let userANumber = {
msisdn: userAResult.international_format_number,
country: userAResult.country_code
};
let userBResult = results[1];
let userBNumber = {
msisdn: userBResult.international_format_number,
country: userBResult.country_code
};
// Create conversation object - for demo purposes:
// - Use first indexed LVN for user A
// - Use second indexed LVN for user B
let conversation = {
userA: {
realNumber: userANumber,
virtualNumber: this.provisionedNumbers[0]
},
userB: {
realNumber: userBNumber,
virtualNumber: this.provisionedNumbers[1]
}
};
this.conversations.push(conversation);
return conversation;
};
Send a confirmation SMS
In a private communication system, when one user contacts another, the caller calls a virtual number from their phone.
Send an SMS to notify each conversation participant of the virtual number they need to call:
/**
* Send an SMS to each conversation participant so they know each other's
* virtual number and can call either other via the proxy.
*/
VoiceProxy.prototype.sendSMS = function(conversation) {
// Send UserA conversation information
// From the UserB virtual number
// To the UserA real number
this.nexmo.message.sendSms(conversation.userB.virtualNumber.msisdn,
conversation.userA.realNumber.msisdn,
'Call this number to talk to UserB');
// Send UserB conversation information
// From the UserA virtual number
// To the UserB real number
this.nexmo.message.sendSms(conversation.userA.virtualNumber.msisdn,
conversation.userB.realNumber.msisdn,
'Call this number to talk to UserB');
return conversation;
};
The users cannot SMS each other. To enable this functionality you need to setup Private SMS communication.
In this use case each user has received the virtual number in an SMS. In other systems this could be supplied using email, in-app notifications, or a predefined number.
Handle inbound calls
When Vonage receives an inbound call to your virtual number it makes a request to the webhook endpoint you set when you created a Voice application:
Vonage Number UserA->>Vonage: Calls virtual number Vonage->>App:Inbound Call(from, to)
Extract to
and from
from the inbound webhook and pass them on to the voice proxy business logic:
app.get('/proxy-call', function(req, res) {
const from = req.query.from;
const to = req.query.to;
const ncco = voiceProxy.getProxyNCCO(from, to);
res.json(ncco);
});
Reverse map real phone numbers to virtual numbers
Now you know the phone number making the call and the virtual number of the recipient, reverse map the inbound virtual number to the outbound real phone number:
for UserB App->>App:Number mapping lookup
The call direction can be identified as:
- The
from
number is UserA real number and theto
number is UserB Vonage number - The
from
number is UserB real number and theto
number is UserA Vonage number
const fromUserAToUserB = function(from, to, conversation) {
return (from === conversation.userA.realNumber.msisdn &&
to === conversation.userB.virtualNumber.msisdn);
};
const fromUserBToUserA = function(from, to, conversation) {
return (from === conversation.userB.realNumber.msisdn &&
to === conversation.userA.virtualNumber.msisdn);
};
/**
* Work out real number to virtual number mapping between users.
*/
VoiceProxy.prototype.getProxyRoute = function(from, to) {
let proxyRoute = null;
let conversation;
for(let i = 0, l = this.conversations.length; i < l; ++i) {
conversation = this.conversations[i];
// Use to and from to determine the conversation
const fromUserA = fromUserAToUserB(from, to, conversation);
const fromUserB = fromUserBToUserA(from, to, conversation);
if(fromUserA || fromUserB) {
proxyRoute = {
conversation: conversation,
to: fromUserA? conversation.userB : conversation.userA,
from: fromUserA? conversation.userA : conversation.userB
};
break;
}
}
return proxyRoute;
};
With the number lookup performed all that's left to do is proxy the call.
Proxy the call
Proxy the call to the phone number the virtual number is associated with. The from
number is always the virtual number, the to
is a real phone number.
call to UserB's
real number Vonage->>UserB: Call Note over UserA,UserB:UserA has called
UserB. But UserA
does not have
the real number
of UserB, nor
vice versa.
In order to do this, create an NCCO (Nexmo Call Control Object). This NCCO uses a talk
action to read out some text. When the talk
has completed, a connect
action forwards the call to a real number.
/**
* Build the NCCO response to instruct Nexmo how to handle the inbound call.
*/
VoiceProxy.prototype.getProxyNCCO = function(from, to) {
// Determine how the call should be routed
const proxyRoute = this.getProxyRoute(from, to);
if(proxyRoute === null) {
const errorText = 'No conversation found' +
' from: ' + from +
' to: ' + to;
throw new Error(errorText);
}
// Build the NCCO
let ncco = [];
const textAction = {
action: 'talk',
text: 'Please wait whilst we connect your call'
};
ncco.push(textAction);
const connectAction = {
action: 'connect',
from: proxyRoute.from.virtualNumber.msisdn,
endpoint: [{
type: 'phone',
number: proxyRoute.to.realNumber.msisdn
}]
};
ncco.push(connectAction);
return ncco;
};
The NCCO is returned to Vonage by the web server.
app.get('/proxy-call', function(req, res) {
const from = req.query.from;
const to = req.query.to;
const ncco = voiceProxy.getProxyNCCO(from, to);
res.json(ncco);
});
Conclusion
You have learned how to build a voice proxy for private communication. You provisioned and configured phone numbers, performed number insight, mapped real numbers to virtual numbers to ensure anonymity, handled an inbound call and proxied the call to another user.