Masked Calling

This guide shows you how to implement the idea described in the Private Voice Communication Use Case. It teaches you how to build a voice proxy with Vonage's Node Server SDK, using virtual numbers to hide participants' real phone numbers. Full source code is also available in our Voice Proxy using the Voice API GitHub repo. Alternatively, you can head to Code Hub to try out the Masked Calling API.

Overview

In some cases, you may need two users to communicate without revealing their personal phone numbers. For example, in a ride-sharing service, users need to coordinate pick-up times without disclosing their contact details, which also helps prevent direct arrangements that could cut into your revenue.

With Vonage's APIs, you can provide participants with temporary numbers that mask their real phone numbers during the call. Once the call ends, the temporary numbers are revoked, ensuring privacy.

Steps

This guide provides instructions for building the application, including:

Prerequisites

In order to work through this use case you need:

Code Repository

There is a GitHub repository containing the code.

Configuration

You need to create a .env file containing the configuration. Instructions for doing that are explained in the GitHub README. As you work through this guide, 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. It should not be confused with the application you will 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 endpoint that Vonage's APIs will make a request to when you receive an inbound call on your virtual number, and the second is the endpoint where the API can post event data.

Replace the domain name in the following Vonage CLI command with your ngrok domain name (To learn more about how to do this, refer to the How to run ngrok guide) 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 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 the Proxy the Call chapter of this guide. For now, be aware that this class initializes the Vonage Server SDK using the API key and secret that you will configure in the next step. This setup 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:

UserBUserAVonageAppUserBUserAVonageAppInitializationSearch NumbersNumbers FoundProvision NumbersNumbers ProvisionedConfigure NumbersNumbers Configured

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 of numbers may require you to provide more information, such as a postal address. If you are not able to obtain a number programmatically, you can visit the Developer Dashboard to make a request for such numbers. Keep in mind that some numbers may require information to be asked and the process will not be fully automated.

When an event occurs related to any number associated with an application, Vonage will send a request to your webhook endpoint with information about the event. After the configuration, be sure to 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 necessary to mask communication between your users.

NOTE: In a production application, you will choose from a pool of virtual numbers. However, it's important to keep this functionality in place to rent additional numbers on the fly.

Create a Call

The workflow to create a call is as presented in the diagram:

UserBUserAVonageAppUserBUserAVonageAppConversation StartsBasic Number InsightNumber Insight responseMap Real/Virtual Numbersfor Each ParticipantSMS to UserASMSSMS to UserBSMS

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 set up Private SMS communication.

In this guide, 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 API Application:

UserBUserAVonageAppUserBUserAVonageAppUserA calls UserB'sVonage NumberCalls virtual numberInbound 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:

UserBUserAVonageAppUserBUserAVonageAppFind the real number for UserBNumber mapping lookup

The call direction can be identified as:

  • The from number is UserA real number and the to number is UserB Vonage number
  • The from number is UserB real number and the to 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, and the to is a real phone number.

UserBUserAVonageAppUserBUserAVonageAppProxy Inboundcall to UserB'sreal numberUserA has calledUserB. But UserAdoes not have the real numberof UserB, nor vice versa.Connect (proxy)Call

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.

Further Information