In this MessageBird Developer Tutorial, you’ll learn how to anonymize and protect your users’ personal information by building a basic masked numbers application powered by the MessageBird API.
Online service platforms—such as ride-sharing, online food delivery, and logistics—facilitate the experience between customers and providers by matching both sides of the transaction to ensure everything runs smoothly and the transaction is completed.
Sometimes it’s necessary for customers and providers to talk or message each other directly; a problem arises since, for many reasons, both parties may not feel comfortable sharing their personal phone number. A great solution to this issue is using anonymous proxy phone numbers that mask a user's personal phone number while also protect the platform's personal contact details. The result: a customer doesn't see their provider's phone number but a number that belongs to the platform and forwards their call to the provider, and vice versa for providers.
Along this tutorial, we'll show you how to build a proxy system to mask phone numbers in Node.js for our fictitious ride-sharing platform, BirdCar. The sample application includes a data model for customers, drivers, rides, and proxy phone numbers and allows setting up new rides from an admin interface for demonstration purposes.
Before we dive into building the sample application, let's take a moment to understand the concept of a number pool. The idea is to set up a list of numbers by purchasing one or more virtual mobile numbers from MessageBird and adding them to a database. Whenever a ride is created, the BirdCar application will automatically search the pool for a driver that is available and then assign the ride.
For simplicity and to allow testing with a single number, BirdCar assigns only one number to each ride, not one for each party. If the customer calls or texts this number, is connected to the driver; if the driver rings, the call or text is forwarded to the customer. The incoming caller or message sender identification sent from the network is used to determine which party calls and consequently find the other party's number.
Relying on the caller identification has the additional advantage that you don’t have to purchase a new phone number for each transaction; instead, it is possible to assign the same one to multiple transactions as long as different people are involved. The ride can be looked up based on who is calling. It is also possible to recycle numbers even for the same customer or driver, that is, returning them to the pool, although we haven’t implemented this behavior in the sample code. In any case, the number should remain active for some time after a transaction has ended, just in case the driver and customer need to communicate afterwards; for example, if the customer has forgotten an item in the driver’s car. 😱
First things first, Our sample application is built in Node.js using the Express framework, so you’ll need Node and npm, you can easily install them for free.
Our application also uses a relational database to store the data model. We bundled SQLite with the application so that you don’t have to set up an RDBMS like MySQL, but if you extend the code for production use, you can still reuse the SQL queries with other databases.
The source code is available in the MessageBird Developer Tutorials GitHub repository, which you can either clone with git or from where you can download a ZIP file with the source code to your computer.
Let's now open the directory where you've stored the sample code and run the following command to install the MessageBird SDK and other dependencies:
npm install
The BirdCar system receives incoming messages and calls and forwards them. From a high-level viewpoint, receiving is relatively simple: an application defines a webhook URL, which you assign to a number purchased in the MessageBird Dashboard using Flow Builder. A webhook is a URL on your site that doesn't render a page to users but is like an API endpoint that can be triggered by other servers. Every time someone sends a message or calls that number, MessageBird collects it and forwards it to the webhook URL where you can process it.
When working with webhooks, an external service like MessageBird needs to access your application, so the webhook URL must be public; however, during development you're typically working in a local development environment that is not publicly available. Thankfully this is not a big deal since various tools and services allow you to quickly expose your development environment to the Internet by providing a tunnel from a public URL to your local machine. One of these tools is localtunnel.me, which is uniquely suited to Node.js developers since you can easily install it using npm:
npm install -g localtunnel
You can start a tunnel by providing a local port number on which your application runs. Our application is configured to run on port 8080, so you can launch localtunnel with the following command:
lt --port 8080
After you've started the tunnel, localtunnel displays your temporary public URL. We'll need that in a minute.
Another common tool for tunneling your local machine is ngrok, which works virtually in the same way; you can have a look at it if you're facing problems with localtunnel.
A requirement for receiving messages is a dedicated inbound number. Virtual mobile numbers (VNM) look and work similar to regular mobile numbers; however, instead of being attached to a mobile device via a SIM card, they live in the cloud and can process inbound SMS and voice calls. MessageBird offers numbers from different countries for a low monthly fee; feel free to explore our low-cost programmable and configurable numbers.
Purchasing a number is quite easy:
Go to the ‘Numbers’ section in the left-hand side of your Dashboard and click the blue button ‘Buy a number’ in the top-right side of your screen.
Pick the country in which you and your customers are located, and make sure both the SMS and Voice capabilities are selected.
Choose one number from the selection and the duration for which you want to pay now.
Confirm by clicking ‘Buy Number’ in the bottom-right of your screen.
Awesome, you’ve set up your first virtual mobile number! 🎉 One is enough for testing, but for real usage of the masked number system, you'll need a larger pool of numbers; simply follow the same steps listed to purchase more.
Pro-tip: Check out our Help Center for more information about virtual mobile numbers and country restrictions.
So you have a number now, but MessageBird has no idea what to do with it. That's why now you need to define a Flow that links your number to your webhook. We’ll start with the flow for inbound SMS messages:
Go to Flow Builder, choose the template ‘Call HTTP endpoint with SMS’, and click ‘Try this flow’.
This template has two steps. Click on the first step ‘SMS’ and select the number or numbers you’d like to attach the flow to. Now, click on the second step ‘Forward to URL’ and choose POST as the method; copy the output from the ngrok command in the URL and add /webhook at the end—this is the name of the route we use to handle incoming messages in our sample application. Click on ‘Save’ when ready.
Ready! Hit ‘Publish’ on the right top of the screen to activate your flow. Well done, another step closer to testing incoming messages! Your flow should look something like this:
Pro-tip: It might be useful to rename it this flow, because Untitled flow won't be helpful in the long run. You can do this by clicking on the icon next to button ‘Back to Overview’ and pressing ‘Rename flow’.
Awesome! 🎉
Let’s set up a second flow for incoming voice calls:
Go back to Flow Builder and hit the button ‘Create new flow’ and then ‘Create Custom Flow’.
Give your flow a name, choose ‘Phone Call’ as the trigger and hit ‘Next’.
Click on the first step ‘Phone Call’ and select the number or numbers you’d like to attach the flow to.
Add a new step by pressing the small ‘+’, choose ‘Fetch call flow from URL’ and paste the same localtunnel base URL into the form, but this time append /webhook-voice to it—this is the name of the route we use to handle incoming calls in our sample application. Click on ‘Save’ when ready.
Ready! Hit ‘Publish’ on the right top of the screen to activate your flow. Your flow should look something like this:
You're done setting up flows for your application! Now, we can begin writing routes in your application for the /webhook and /webhook-voice URL paths that these flows are using.
The MessageBird SDK and an API key are not required to receive messages; however, since we want to send and forward messages, we need to add and configure it. The SDK is defined in package.json and loaded with a statement in HomePageFooter.js:
// Load and initialize MessageBird SDKvar messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY);
You need to provide a MessageBird API key via an environment variable loaded with dotenv. We've prepared an env.example file in the repository, which you should rename to .env and add the required information. Here's an example:
MESSAGEBIRD_API_KEY=YOUR-API-KEY
First, go to the MessageBird Dashboard; if you have already created an API key it will be shown right there. If you don’t see any key on the Dashboard or if you're unsure whether this key is in live mode, go to the Developers section in the MessageBird Dashboard and open the API access (REST) tab. There you can create new API keys and manage your existing ones.
If you are having any issues creating your API key, please reach out to support@messagebird.com; we’ll make sure to help you out.
Our BirdCar application uses a relational model; we have the following four entities:
Open the file createdb.js in the repository. It contains four CREATE TABLE queries to set up the data model. Below that, you'll find some INSERT INTO queries to add sample customers, drivers, and proxy numbers. Update those queries like this:
After updating the file, save it and run the following command (if you already have localtunnel running open a second command prompt for it):
node createdb.js
Keep in mind that this command only works once so if you make changes and want to recreate the database, you must delete the file ridesharing.db that the script creates before re-running it:
rm ridesharing.dbnode createdb.js
The app.get('/') route in HomePageFooter.js and the associated HTML page in views/admin.handlebars implement a simple homepage that lists the content from the database and provides a form to add a new ride. For creating a ride, an admin can select a customer and driver from a drop-down, and enter start, destination, date and time; the form submits this information to /createride.
The app.post('/createride') route defined in HomePageFooter.js handles the following steps when creating a new ride:
The form fields contain only IDs for customer and driver, so we’ll make a query for each to find all the information that we need in subsequent steps:
// Create a new rideapp.post('/createride', function(req, res) {// Find customer detailsdb.get("SELECT * FROM customers WHERE id = $id", { $id : req.body.customer }, function(err, row) {var customer = row;// Find driver detailsdb.get("SELECT * FROM drivers WHERE id = $id", { $id : req.body.driver }, function(err, row) {var driver = row;
We need to get a number from the pool that was never been assigned to a ride for the customer or the driver. To check this, let’s write a SQL query with two subqueries:
In Javascript and SQL, this check looks like this:
// Find a number that has not been used by the driver or the customerdb.get("SELECT * FROM proxy_numbers "+ "WHERE id NOT IN (SELECT number_id FROM rides WHERE customer_id = $customer) "+ "AND id NOT IN (SELECT number_id FROM rides WHERE driver_id = $driver)", {$customer : customer.id,$driver : driver.id,}, function(err, row) {
It's possible that no row was found; in that case, we alert the admin that the number pool is depleted and they should buy more numbers:
if (row == null) {// No number found!res.send("No number available! Please extend your pool.");
Once a number was found, that is, our query returned a row, it’s time to insert a new ride into the database using the information from the form:
} else {var proxyNumber = row;// Store ride in databasedb.run("INSERT INTO rides (start, destination, datetime, customer_id, driver_id, number_id) VALUES ($start, $destination, $datetime, $customer, $driver,$number)", {$start : req.body.start,$destination : req.body.destination,$datetime : req.body.datetime,$customer : customer.id,$driver : driver.id,$number : proxyNumber.id});
We should now send a message to both the customer and the driver to confirm the ride. This message should originate from the proxy number, so they can quickly reply to this message to reach the other party. For sending messages, the MessageBird SDK provides the messagebird.messages.create() function. We need to call the function twice because we're sending two different versions of the message:
// Notify the customermessagebird.messages.create({originator: proxyNumber.number,recipients: [customer.number],body: driver.name + ' will pick you up at ' + req.body.datetime + '. Reply to this message to contact the driver.',},function(err, response) {console.log(err, response);},);// Notify the drivermessagebird.messages.create({originator: proxyNumber.number,recipients: [driver.number],body:customer.name + ' will wait for you at ' + req.body.datetime + '. Reply to this message to contact the customer.',},function(err, response) {console.log(err, response);},);
The response, or error, if any, is logged to the console, MessageBird doesn’t read or take any action based on them. In production applications you should definitely check if the messages were sent successfully and implement some more sophisticated error handling.
When a customer or driver replies to the message confirming their ride, the response should go to the other party. As we have instructed MessageBird to post to /webhook, we need to implement the app.post('/webhook'); route.
First, we read the input sent from MessageBird. We're interested in three fields: originator, payload (the message text), and recipient (the virtual number to which the user sent their message), so that we can find the ride based on this information:
// Handle incoming messagesapp.post('/webhook', function(req, res) {// Read input sent from MessageBirdvar number = req.body.originator;var text = req.body.payload;var proxy = req.body.recipient;
To find the ride we use an SQL query which joins all four tables. We're interested in all entries in which the proxy number matches the recipient field from the webhook, and the originator matches either the driver's number or the customer's number:
db.get("SELECT c.number AS customer_number, d.number AS driver_number, p.number AS proxy_number "+ "FROM rides r JOIN customers c ON r.customer_id = c.id JOIN drivers d ON r.driver_id = d.id JOIN proxy_numbers p ON p.id = r.number_id "+ "WHERE proxy_number = $proxy AND (driver_number = $number OR customer_number = $number)", {$number : number,$proxy : proxy}, function(err, row) {
After we've found the ride based on an or-condition, we need to check again which party was the actual sender and determine the recipient (the other party) from there:
if (row) {// Got a match!// Need to find out whether customer or driver sent this and forward to the other sidevar recipient = "";if (number == row.customer_number)recipient = row.driver_number;elseif (number == row.driver_number)recipient = row.customer_number;
We use messagebird.messages.create() to forward the message. The proxy number is used as the originator, and we send the original text to the recipient as determined above:
// Forward the message through the MessageBird APImessagebird.messages.create({originator: proxy,recipients: [recipient],body: text,},function(err, response) {console.log(err, response);},);
If we don't find a ride, we log an error to the console:
} else {// Cannot match numbersconsole.log("Could not find a ride for customer/driver " + number + " that uses proxy " + proxy + ".");}
When a customer or driver calls the proxy number from which they received the confirmation, the system should transfer the call to the other party. As we have instructed MessageBird to fetch instructions from /webhook-voice, we need to implement the app.get('/webhook-voice'); route. Keep in mind that unlike the SMS webhook, where we have configured POST, custom call flows are always retrieved with GET.
First, the input sent from MessageBird should be read because we're interested in the source and destination of the call so that we can find the ride based on this information:
// Handle incoming callsapp.get('/webhook-voice', function(req, res) {// Read input sent from MessageBirdvar number = req.query.source;var proxy = req.query.destination;
As we’ll return a new call flow encoded in XML format, let’s set the response header accordingly:
// Answer will always be XMLres.set('Content-Type', 'application/xml');
This works exactly as described for the SMS webhooks; therefore, the SQL query and surrounding Javascript code are mostly a verbatim copy. If you’re extending the sample to build a production application, it could be a good idea to make a function as an abstraction around it to avoid duplicate code.
To transfer the call, we return a short snippet of XML to MessageBird and also log the action to the console:
// Create call flow to instruct transferconsole.log('Transferring call to ' + recipient);res.send('<?xml version="1.0" encoding="UTF-8"?>' + '<Transfer destination="' + recipient + '" mask="true" />');
The <Transfer /> element takes two attributes: destination indicates the number to transfer the call to—which we've determined as described above—and mask instructs MessageBird to use the proxy number instead of the original caller ID.
If we don't find a ride, we return a different XML snippet with a <Say /> element, which is used to read some instructions to the caller:
} else {// Cannot match numbersres.send('<?xml version="1.0" encoding="UTF-8"?>'+ '<Say language="en-GB" voice="female">Sorry, we cannot identify your transaction. Make sure you call in from the number you registered.</Say>');}
This element takes two attributes, language and voice, that define the configuration for speech synthesis. The text itself goes between the opening and closing XML element.
Make sure you’ve set up at least one number correctly with two flows to forward both incoming messages and incoming phone calls to a localtunnel URL, and that the tunnel is still running. Keep in mind that whenever you start a fresh tunnel, you'll get a new URL, so you also have to update it in the flows accordingly. You can also configure a more permanent URL using the -s attribute with the lt command.
To start the sample application you have to enter another command, but your existing console window is now busy running your tunnel, so you need to open another one. With Mac you can press Command + Tab to open a second tab that's already pointed to the correct directory. With other operating systems you may have to open another console window manually. Either way, once you've got a command prompt, type the following to start the application:
node HomePageFooter.js
Open http://localhost:8080/ in your browser and create a ride between the customer and driver you configured in dbcreate.js. If everything works out correctly, two phones should receive a message. Reply to the incoming message on one phone and you'll receive this reply on the other phone, but magically coming from the proxy number. Lovely! You can also test voice call forwarding as well: simply call the proxy number from one phone and magically see the other phone ring.
If you didn't get the messages or the forwarding doesn't work, check the console output from Node to see if there's any problem with the API—such as an incorrect API key or a typo in one of the numbers—and try again.
Awesome! Use the flow, code snippets, and UI examples from this tutorial as an inspiration to build your own application. Don't forget to download the code from the MessageBird Developer Tutorials GitHub repository.
Nice work! 🎉
You've just built your own number masking system with MessageBird with Node.js!
Want to build something similar but not quite sure how to get started? Feel free to let us know at support@messagebird.com; we'd love to help!