This Express sample application is modeled after the amazing rental experience created by AirBnB, but with more Klingons.
Host users can offer rental properties which other guest users can reserve. The guest and the host can then anonymously communicate via a disposable Twilio phone number created just for a reservation. In this tutorial, we'll show you the key bits of code to make this work.
To run this sample app yourself, download the code and follow the instructions on GitHub.
If you choose to manage communications between your users, including voice calls, text-based messages (e.g., SMS), and chat, you may need to comply with certain laws and regulations, including those regarding obtaining consent. Additional information regarding legal compliance considerations and best practices for using Twilio to manage and record communications between your users, such as when using Twilio Proxy, can be found here.
Notice: Twilio recommends that you consult with your legal counsel to make sure that you are complying with all applicable laws in connection with communications you record or store using Twilio.
Read how Lyft uses masked phone numbers to let customers securely contact drivers.
The first step in connecting a guest and host is creating a reservation. Here we handle a form submission for a new reservation which contains the message and the guest's information which is grabbed from the logged in user.
routes/reservations.js
_108var twilio = require('twilio');_108var MessagingResponse = require('twilio').twiml.MessagingResponse;_108var express = require('express');_108var router = express.Router();_108var Property = require('../models/property');_108var Reservation = require('../models/reservation');_108var User = require('../models/user');_108var notifier = require('../lib/notifier');_108var purchaser = require('../lib/purchaser');_108var middleware = require('../lib/middleware');_108_108router.get('/', middleware.isAuthenticated, function (req, res) {_108 var userId = req.user.id;_108 Reservation.find({})_108 .deepPopulate('property property.owner guest')_108 .then(function (reservations) {_108 var hostReservations = reservations.filter(function (reservation) {_108 return reservation.property.owner.id === userId;_108 });_108_108 res.render('reservations/index', { reservations: hostReservations, user: req.user });_108 });_108});_108_108// POST: /reservations_108router.post('/', function (req, res) {_108 var propertyId = req.body.propertyId;_108 var user = req.user;_108_108 Property.findOne({ _id: propertyId })_108 .then(function (property) {_108 var reservation = new Reservation({_108 message: req.body.message,_108 property: propertyId,_108 guest: user.id_108 });_108_108 return reservation.save();_108 })_108 .then(function () {_108 notifier.sendNotification();_108 res.redirect('/properties');_108 })_108 .catch(function(err) {_108 console.log(err);_108 });_108});_108_108// POST: /reservations/handle_108router.post('/handle', twilio.webhook({validate: false}), function (req, res) {_108 var from = req.body.From;_108 var smsRequest = req.body.Body;_108_108 var smsResponse;_108_108 User.findOne({phoneNumber: from})_108 .then(function (host) {_108 return Reservation.findOne({status: 'pending'})_108 .deepPopulate('property property.owner guest')_108 })_108 .then(function (reservation) {_108 if (reservation === null) {_108 throw 'No pending reservations';_108 }_108_108 var hostAreaCode = reservation.property.owner.areaCode;_108_108 var phoneNumber = purchaser.purchase(hostAreaCode);_108 var reservationPromise = Promise.resolve(reservation);_108_108 return Promise.all([phoneNumber, reservationPromise]);_108 })_108 .then(function (data) {_108 var phoneNumber = data[0];_108 var reservation = data[1];_108_108 if (isSmsRequestAccepted(smsRequest)) {_108 reservation.status = "confirmed";_108 reservation.phoneNumber = phoneNumber;_108 } else {_108 reservation.status = "rejected";_108 }_108 return reservation.save();_108 })_108 .then(function (reservation) {_108 var message = "You have successfully " + reservation.status + " the reservation";_108 respond(res, message);_108 })_108 .catch(function (err) {_108 console.log(err);_108 var message = "Sorry, it looks like you do not have any reservations to respond to";_108 respond(res, message);_108 });_108});_108_108var isSmsRequestAccepted = function (smsRequest) {_108 return smsRequest.toLowerCase() === 'accept';_108};_108_108var respond = function(res, message) {_108 var messagingResponse = new MessagingResponse();_108 messagingResponse.message(message);_108_108 res.type('text/xml');_108 res.send(messagingResponse.toString());_108}_108_108module.exports = router;
Part of our reservation system is receiving reservation requests from potential renters. However, these reservations need to be confirmed. Let's see how we would handle this step.
Before the reservation is finalized, the host needs to confirm that the property was reserved. Learn how to automate this process in our first AirTNG tutorial, Workflow Automation.
routes/reservations.js
_108var twilio = require('twilio');_108var MessagingResponse = require('twilio').twiml.MessagingResponse;_108var express = require('express');_108var router = express.Router();_108var Property = require('../models/property');_108var Reservation = require('../models/reservation');_108var User = require('../models/user');_108var notifier = require('../lib/notifier');_108var purchaser = require('../lib/purchaser');_108var middleware = require('../lib/middleware');_108_108router.get('/', middleware.isAuthenticated, function (req, res) {_108 var userId = req.user.id;_108 Reservation.find({})_108 .deepPopulate('property property.owner guest')_108 .then(function (reservations) {_108 var hostReservations = reservations.filter(function (reservation) {_108 return reservation.property.owner.id === userId;_108 });_108_108 res.render('reservations/index', { reservations: hostReservations, user: req.user });_108 });_108});_108_108// POST: /reservations_108router.post('/', function (req, res) {_108 var propertyId = req.body.propertyId;_108 var user = req.user;_108_108 Property.findOne({ _id: propertyId })_108 .then(function (property) {_108 var reservation = new Reservation({_108 message: req.body.message,_108 property: propertyId,_108 guest: user.id_108 });_108_108 return reservation.save();_108 })_108 .then(function () {_108 notifier.sendNotification();_108 res.redirect('/properties');_108 })_108 .catch(function(err) {_108 console.log(err);_108 });_108});_108_108// POST: /reservations/handle_108router.post('/handle', twilio.webhook({validate: false}), function (req, res) {_108 var from = req.body.From;_108 var smsRequest = req.body.Body;_108_108 var smsResponse;_108_108 User.findOne({phoneNumber: from})_108 .then(function (host) {_108 return Reservation.findOne({status: 'pending'})_108 .deepPopulate('property property.owner guest')_108 })_108 .then(function (reservation) {_108 if (reservation === null) {_108 throw 'No pending reservations';_108 }_108_108 var hostAreaCode = reservation.property.owner.areaCode;_108_108 var phoneNumber = purchaser.purchase(hostAreaCode);_108 var reservationPromise = Promise.resolve(reservation);_108_108 return Promise.all([phoneNumber, reservationPromise]);_108 })_108 .then(function (data) {_108 var phoneNumber = data[0];_108 var reservation = data[1];_108_108 if (isSmsRequestAccepted(smsRequest)) {_108 reservation.status = "confirmed";_108 reservation.phoneNumber = phoneNumber;_108 } else {_108 reservation.status = "rejected";_108 }_108 return reservation.save();_108 })_108 .then(function (reservation) {_108 var message = "You have successfully " + reservation.status + " the reservation";_108 respond(res, message);_108 })_108 .catch(function (err) {_108 console.log(err);_108 var message = "Sorry, it looks like you do not have any reservations to respond to";_108 respond(res, message);_108 });_108});_108_108var isSmsRequestAccepted = function (smsRequest) {_108 return smsRequest.toLowerCase() === 'accept';_108};_108_108var respond = function(res, message) {_108 var messagingResponse = new MessagingResponse();_108 messagingResponse.message(message);_108_108 res.type('text/xml');_108 res.send(messagingResponse.toString());_108}_108_108module.exports = router;
Once the reservation is confirmed, we need to purchase a Twilio number that the guest and host can use to communicate.
Here we use a Twilio Node Helper Library to search for and buy a new phone number to associate with the reservation. When we buy the number, we designate a Twilio Application that will handle webhook requests when the new number receives an incoming call or text.
We then save the new phone number on our reservation
model, so when our app receives calls or texts to this number, we'll know which reservation the call or text belongs to.
lib/purchaser.js
_26var config = require('../config');_26var client = require('twilio')(config.accountSid, config.authToken);_26_26var purchase = function (areaCode) {_26 var phoneNumber;_26_26 return client.availablePhoneNumbers('US').local.list({_26 areaCode: areaCode,_26 voiceEnabled: true,_26 smsEnabled: true_26 }).then(function(searchResults) {_26 if (searchResults.availablePhoneNumbers.length === 0) {_26 throw { message: 'No numbers found with that area code' };_26 }_26_26 return client.incomingPhoneNumbers.create({_26 phoneNumber: searchResults.availablePhoneNumbers[0].phoneNumber,_26 voiceApplicationSid: config.applicationSid,_26 smsApplicationSid: config.applicationSid_26 });_26 }).then(function(number) {_26 return number.phone_number;_26 });_26}_26_26exports.purchase = purchase;
Now that each reservation has a Twilio Phone Number, we can see how the application will look up reservations as guest or host calls come in.
When someone sends an SMS or calls one of the Twilio numbers you have configured, Twilio makes a request to the URL you set in the Twiml app. In this request, Twilio includes some useful information including:
From
number that originally called or sent an SMS.
To
Twilio number that triggered this request.
Take a look at Twilio's SMS Documentation and Twilio's Voice Documentation for a full list of the parameters you can use.
In our controller we use the to
parameter sent by Twilio to find a reservation that has the number we bought stored in it, as this is the number both hosts and guests will call and send SMS to.
routes/commuter.js
_71var twilio = require('twilio');_71var VoiceResponse = require('twilio').twiml.VoiceResponse;_71var MessagingResponse = require('twilio').twiml.MessagingResponse;_71var express = require('express');_71var router = express.Router();_71var Reservation = require('../models/reservation');_71_71// POST: /commuter/use-sms_71router.post('/use-sms', twilio.webhook({ validate: false }), function (req, res) {_71 from = req.body.From;_71 to = req.body.To;_71 body = req.body.Body;_71_71 gatherOutgoingNumber(from, to)_71 .then(function (outgoingPhoneNumber) {_71 var messagingResponse = new MessagingResponse();_71 messagingResponse.message({ to: outgoingPhoneNumber }, body);_71_71 res.type('text/xml');_71 res.send(messagingResponse.toString());_71 })_71});_71_71// POST: /commuter/use-voice_71router.post('/use-voice', twilio.webhook({ validate: false }), function (req, res) {_71 from = req.body.From;_71 to = req.body.To;_71 body = req.body.Body;_71_71 gatherOutgoingNumber(from, to)_71 .then(function (outgoingPhoneNumber) {_71 var voiceResponse = new VoiceResponse();_71 voiceResponse.play('http://howtodocs.s3.amazonaws.com/howdy-tng.mp3');_71 voiceResponse.dial(outgoingPhoneNumber);_71_71 res.type('text/xml');_71 res.send(voiceResponse.toString());_71 })_71});_71_71var gatherOutgoingNumber = function (incomingPhoneNumber, anonymousPhoneNumber) {_71 var phoneNumber = anonymousPhoneNumber;_71_71 return Reservation.findOne({ phoneNumber: phoneNumber })_71 .deepPopulate('property property.owner guest')_71 .then(function (reservation) {_71 var hostPhoneNumber = formattedPhoneNumber(reservation.property.owner);_71 var guestPhoneNumber = formattedPhoneNumber(reservation.guest);_71_71 // Connect from Guest to Host_71 if (guestPhoneNumber === incomingPhoneNumber) {_71 outgoingPhoneNumber = hostPhoneNumber;_71 }_71_71 // Connect from Host to Guest_71 if (hostPhoneNumber === incomingPhoneNumber) {_71 outgoingPhoneNumber = guestPhoneNumber;_71 }_71_71 return outgoingPhoneNumber;_71 })_71 .catch(function (err) {_71 console.log(err);_71 });_71}_71_71var formattedPhoneNumber = function(user) {_71 return "+" + user.countryCode + user.areaCode + user.phoneNumber;_71};_71_71module.exports = router;
Next, let's see how to connect the guest and the host via SMS.
Our Twilio application should be configured to send HTTP requests to this controller method on any incoming text message. Our app responds with TwiML to tell Twilio what to do in response to the message.
If the initial message sent to the anonymous number was sent by the host, we forward it on to the guest. Conversely, if the original message was sent by the guest, we forward it to the host.
To find the outgoing number we'll use the gatherOutgoingNumber
helper method.
routes/commuter.js
_71var twilio = require('twilio');_71var VoiceResponse = require('twilio').twiml.VoiceResponse;_71var MessagingResponse = require('twilio').twiml.MessagingResponse;_71var express = require('express');_71var router = express.Router();_71var Reservation = require('../models/reservation');_71_71// POST: /commuter/use-sms_71router.post('/use-sms', twilio.webhook({ validate: false }), function (req, res) {_71 from = req.body.From;_71 to = req.body.To;_71 body = req.body.Body;_71_71 gatherOutgoingNumber(from, to)_71 .then(function (outgoingPhoneNumber) {_71 var messagingResponse = new MessagingResponse();_71 messagingResponse.message({ to: outgoingPhoneNumber }, body);_71_71 res.type('text/xml');_71 res.send(messagingResponse.toString());_71 })_71});_71_71// POST: /commuter/use-voice_71router.post('/use-voice', twilio.webhook({ validate: false }), function (req, res) {_71 from = req.body.From;_71 to = req.body.To;_71 body = req.body.Body;_71_71 gatherOutgoingNumber(from, to)_71 .then(function (outgoingPhoneNumber) {_71 var voiceResponse = new VoiceResponse();_71 voiceResponse.play('http://howtodocs.s3.amazonaws.com/howdy-tng.mp3');_71 voiceResponse.dial(outgoingPhoneNumber);_71_71 res.type('text/xml');_71 res.send(voiceResponse.toString());_71 })_71});_71_71var gatherOutgoingNumber = function (incomingPhoneNumber, anonymousPhoneNumber) {_71 var phoneNumber = anonymousPhoneNumber;_71_71 return Reservation.findOne({ phoneNumber: phoneNumber })_71 .deepPopulate('property property.owner guest')_71 .then(function (reservation) {_71 var hostPhoneNumber = formattedPhoneNumber(reservation.property.owner);_71 var guestPhoneNumber = formattedPhoneNumber(reservation.guest);_71_71 // Connect from Guest to Host_71 if (guestPhoneNumber === incomingPhoneNumber) {_71 outgoingPhoneNumber = hostPhoneNumber;_71 }_71_71 // Connect from Host to Guest_71 if (hostPhoneNumber === incomingPhoneNumber) {_71 outgoingPhoneNumber = guestPhoneNumber;_71 }_71_71 return outgoingPhoneNumber;_71 })_71 .catch(function (err) {_71 console.log(err);_71 });_71}_71_71var formattedPhoneNumber = function(user) {_71 return "+" + user.countryCode + user.areaCode + user.phoneNumber;_71};_71_71module.exports = router;
Let's see how to connect the guest and the host via phone call next.
That's it! We've just implemented anonymous communications that allow your customers to connect while protecting their privacy.
If you're a Node.js developer working with Twilio, you might want to check out these other tutorials:
Easily route callers to the right people and information with an IVR (interactive voice response) system.
Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages. Learn how to create your own survey in the language and framework of your choice.
Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think.