This ASP.NET 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.
AirTNG.Web/Controllers/ReservationsController.cs
_150using System;_150using System.Threading.Tasks;_150using System.Web.Mvc;_150using AirTNG.Web.Domain.PhoneNumber;_150using AirTNG.Web.Domain.Reservations;_150using AirTNG.Web.Models;_150using AirTNG.Web.Models.Repository;_150using AirTNG.Web.ViewModels;_150using Microsoft.AspNet.Identity;_150using Twilio.TwiML;_150using Twilio.TwiML.Mvc;_150_150namespace AirTNG.Web.Controllers_150{_150 [Authorize]_150 public class ReservationsController : TwilioController_150 {_150 private readonly IVacationPropertiesRepository _vacationPropertiesRepository;_150 private readonly IReservationsRepository _reservationsRepository;_150 private readonly IUsersRepository _usersRepository;_150 private readonly INotifier _notifier;_150 private readonly IPurchaser _phoneNumberPurchaser;_150_150 public Func<string> UserId;_150_150 public ReservationsController() : this(_150 new VacationPropertiesRepository(),_150 new ReservationsRepository(),_150 new UsersRepository(),_150 new Notifier(),_150 new Purchaser()) { }_150_150 public ReservationsController(_150 IVacationPropertiesRepository vacationPropertiesRepository,_150 IReservationsRepository reservationsRepository,_150 IUsersRepository usersRepository,_150 INotifier notifier,_150 IPurchaser phoneNumberPurchaser)_150 {_150 _vacationPropertiesRepository = vacationPropertiesRepository;_150 _reservationsRepository = reservationsRepository;_150 _usersRepository = usersRepository;_150 _notifier = notifier;_150 _phoneNumberPurchaser = phoneNumberPurchaser;_150 UserId = () => User.Identity.GetUserId();_150 }_150_150 public async Task<ActionResult> Index()_150 {_150 var user = await _usersRepository.FindAsync(UserId());_150 var reservations = user.Reservations;_150_150 return View(reservations);_150 }_150_150 // GET: Reservations/Create_150 public async Task<ActionResult> Create(int id)_150 {_150 var vacationProperty = await _vacationPropertiesRepository.FindAsync(id);_150 var reservation = new ReservationViewModel_150 {_150 ImageUrl = vacationProperty.ImageUrl,_150 Description = vacationProperty.Description,_150 VacationPropertyId = vacationProperty.Id,_150 VacationPropertyDescription = vacationProperty.Description,_150 UserName = vacationProperty.Owner.Name,_150 UserPhoneNumber = vacationProperty.Owner.PhoneNumber,_150 };_150_150 return View(reservation);_150 }_150_150 // POST: Reservations/Create_150 [HttpPost]_150 public async Task<ActionResult> Create(ReservationViewModel model)_150 {_150 if (ModelState.IsValid)_150 {_150 var reservation = new Reservation_150 {_150 Message = model.Message,_150 UserId = UserId(), // This is the reservee user ID_150 VactionPropertyId = model.VacationPropertyId,_150 Status = ReservationStatus.Pending,_150 CreatedAt = DateTime.Now_150 };_150_150 await _reservationsRepository.CreateAsync(reservation);_150 await _reservationsRepository.LoadNavigationPropertiesAsync(reservation);_150_150 await _notifier.SendNotificationAsync(reservation);_150_150 return RedirectToAction("Index", "VacationProperties");_150 }_150_150 return View(model);_150 }_150_150 // POST Reservations/Handle_150 [HttpPost]_150 [AllowAnonymous]_150 public async Task<ActionResult> Handle(string from, string body)_150 {_150 string smsResponse;_150_150 try_150 {_150 var host = await _usersRepository.FindByPhoneNumberAsync(from);_150 var reservation = await _reservationsRepository.FindFirstPendingReservationByHostAsync(host.Id);_150_150 var smsRequest = body;_150 if (IsSmsRequestAccepted(smsRequest))_150 {_150 var purchasedPhoneNumber = _phoneNumberPurchaser.Purchase(host.AreaCode);_150_150 reservation.Status = ReservationStatus.Confirmed;_150 reservation.AnonymousPhoneNumber = purchasedPhoneNumber.PhoneNumber;_150 }_150 else_150 {_150 reservation.Status = ReservationStatus.Rejected;_150 }_150_150 await _reservationsRepository.UpdateAsync(reservation);_150 smsResponse =_150 string.Format("You have successfully {0} the reservation", reservation.Status);_150 }_150 catch (Exception)_150 {_150 smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";_150 }_150_150 return TwiML(Respond(smsResponse));_150 }_150_150 private static TwilioResponse Respond(string message)_150 {_150 var response = new TwilioResponse();_150 response.Message(message);_150_150 return response;_150 }_150_150 private static bool IsSmsRequestAccepted(string smsRequest)_150 {_150 return smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||_150 smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase);_150 }_150 }_150}
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 finishing with the reservation, the host needs to confirm that the property was reserved. Learn how to automate this process in our first AirTNG tutorial, Workflow Automation.
AirTNG.Web/Controllers/ReservationsController.cs
_150using System;_150using System.Threading.Tasks;_150using System.Web.Mvc;_150using AirTNG.Web.Domain.PhoneNumber;_150using AirTNG.Web.Domain.Reservations;_150using AirTNG.Web.Models;_150using AirTNG.Web.Models.Repository;_150using AirTNG.Web.ViewModels;_150using Microsoft.AspNet.Identity;_150using Twilio.TwiML;_150using Twilio.TwiML.Mvc;_150_150namespace AirTNG.Web.Controllers_150{_150 [Authorize]_150 public class ReservationsController : TwilioController_150 {_150 private readonly IVacationPropertiesRepository _vacationPropertiesRepository;_150 private readonly IReservationsRepository _reservationsRepository;_150 private readonly IUsersRepository _usersRepository;_150 private readonly INotifier _notifier;_150 private readonly IPurchaser _phoneNumberPurchaser;_150_150 public Func<string> UserId;_150_150 public ReservationsController() : this(_150 new VacationPropertiesRepository(),_150 new ReservationsRepository(),_150 new UsersRepository(),_150 new Notifier(),_150 new Purchaser()) { }_150_150 public ReservationsController(_150 IVacationPropertiesRepository vacationPropertiesRepository,_150 IReservationsRepository reservationsRepository,_150 IUsersRepository usersRepository,_150 INotifier notifier,_150 IPurchaser phoneNumberPurchaser)_150 {_150 _vacationPropertiesRepository = vacationPropertiesRepository;_150 _reservationsRepository = reservationsRepository;_150 _usersRepository = usersRepository;_150 _notifier = notifier;_150 _phoneNumberPurchaser = phoneNumberPurchaser;_150 UserId = () => User.Identity.GetUserId();_150 }_150_150 public async Task<ActionResult> Index()_150 {_150 var user = await _usersRepository.FindAsync(UserId());_150 var reservations = user.Reservations;_150_150 return View(reservations);_150 }_150_150 // GET: Reservations/Create_150 public async Task<ActionResult> Create(int id)_150 {_150 var vacationProperty = await _vacationPropertiesRepository.FindAsync(id);_150 var reservation = new ReservationViewModel_150 {_150 ImageUrl = vacationProperty.ImageUrl,_150 Description = vacationProperty.Description,_150 VacationPropertyId = vacationProperty.Id,_150 VacationPropertyDescription = vacationProperty.Description,_150 UserName = vacationProperty.Owner.Name,_150 UserPhoneNumber = vacationProperty.Owner.PhoneNumber,_150 };_150_150 return View(reservation);_150 }_150_150 // POST: Reservations/Create_150 [HttpPost]_150 public async Task<ActionResult> Create(ReservationViewModel model)_150 {_150 if (ModelState.IsValid)_150 {_150 var reservation = new Reservation_150 {_150 Message = model.Message,_150 UserId = UserId(), // This is the reservee user ID_150 VactionPropertyId = model.VacationPropertyId,_150 Status = ReservationStatus.Pending,_150 CreatedAt = DateTime.Now_150 };_150_150 await _reservationsRepository.CreateAsync(reservation);_150 await _reservationsRepository.LoadNavigationPropertiesAsync(reservation);_150_150 await _notifier.SendNotificationAsync(reservation);_150_150 return RedirectToAction("Index", "VacationProperties");_150 }_150_150 return View(model);_150 }_150_150 // POST Reservations/Handle_150 [HttpPost]_150 [AllowAnonymous]_150 public async Task<ActionResult> Handle(string from, string body)_150 {_150 string smsResponse;_150_150 try_150 {_150 var host = await _usersRepository.FindByPhoneNumberAsync(from);_150 var reservation = await _reservationsRepository.FindFirstPendingReservationByHostAsync(host.Id);_150_150 var smsRequest = body;_150 if (IsSmsRequestAccepted(smsRequest))_150 {_150 var purchasedPhoneNumber = _phoneNumberPurchaser.Purchase(host.AreaCode);_150_150 reservation.Status = ReservationStatus.Confirmed;_150 reservation.AnonymousPhoneNumber = purchasedPhoneNumber.PhoneNumber;_150 }_150 else_150 {_150 reservation.Status = ReservationStatus.Rejected;_150 }_150_150 await _reservationsRepository.UpdateAsync(reservation);_150 smsResponse =_150 string.Format("You have successfully {0} the reservation", reservation.Status);_150 }_150 catch (Exception)_150 {_150 smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";_150 }_150_150 return TwiML(Respond(smsResponse));_150 }_150_150 private static TwilioResponse Respond(string message)_150 {_150 var response = new TwilioResponse();_150 response.Message(message);_150_150 return response;_150 }_150_150 private static bool IsSmsRequestAccepted(string smsRequest)_150 {_150 return smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||_150 smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase);_150 }_150 }_150}
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 C# Helper Library to search for and buy a new phone number to associate with the reservation. When we purchase 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.
AirTNG.Web/Domain/PhoneNumber/Purchaser.cs
_55using System.Linq;_55using AirTNG.Web.Domain.Twilio;_55using Twilio;_55_55namespace AirTNG.Web.Domain.PhoneNumber_55{_55 public interface IPurchaser_55 {_55 IncomingPhoneNumber Purchase(string areaCode);_55 }_55_55 public class Purchaser : IPurchaser_55 {_55 private readonly TwilioRestClient _client;_55_55 public Purchaser() : this(new TwilioRestClient(Credentials.AccountSID, Credentials.AuthToken)) { }_55_55 public Purchaser(TwilioRestClient client)_55 {_55 _client = client;_55 }_55_55 /// <summary>_55 /// Purchase the first available phone number._55 /// </summary>_55 /// <param name="areaCode">The area code</param>_55 /// <returns>The purchased phone number</returns>_55 public IncomingPhoneNumber Purchase(string areaCode)_55 {_55 var phoneNumberOptions = new PhoneNumberOptions_55 {_55 PhoneNumber = SearchForFirstAvailablePhoneNumber(areaCode),_55 VoiceApplicationSid = Credentials.ApplicationSID_55 };_55_55 return _client.AddIncomingPhoneNumber(phoneNumberOptions);_55 }_55_55 private string SearchForFirstAvailablePhoneNumber(string areaCode)_55 {_55 var searchParams = new AvailablePhoneNumberListRequest_55 {_55 AreaCode = areaCode,_55 VoiceEnabled = true,_55 SmsEnabled = true_55 };_55_55 return _client_55 .ListAvailableLocalPhoneNumbers("US", searchParams)_55 .AvailablePhoneNumbers_55 .First() // We're only interested in the first available phone number._55 .PhoneNumber;_55 }_55 }_55}
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 initially 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.
AirTNG.Web/Controllers/PhoneExchangeController.cs
_66using System.Threading.Tasks;_66using System.Web.Mvc;_66using AirTNG.Web.Models.Repository;_66using Twilio.TwiML;_66using Twilio.TwiML.Mvc;_66_66namespace AirTNG.Web.Controllers_66{_66 public class PhoneExchangeController : TwilioController_66 {_66 private readonly IReservationsRepository _repository;_66_66 public PhoneExchangeController() : this(new ReservationsRepository()) { }_66_66 public PhoneExchangeController(IReservationsRepository repository)_66 {_66 _repository = repository;_66 }_66_66 // POST: PhoneExchange/InterconnectUsingSms_66 [HttpPost]_66 public async Task<ActionResult> InterconnectUsingSms(string from, string to, string body)_66 {_66 var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);_66_66 var response = new TwilioResponse();_66 response.Message(body, new {to = outgoingPhoneNumber});_66_66 return TwiML(response);_66 }_66_66 // POST: PhoneExchange/InterconnectUsingVoice_66 [HttpPost]_66 public async Task<ActionResult> InterconnectUsingVoice(string from, string to)_66 {_66 var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);_66_66 var response = new TwilioResponse();_66 response.Play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3");_66 response.Dial(outgoingPhoneNumber);_66_66 return TwiML(response);_66 }_66_66 private async Task<string> GatherOutgoingPhoneNumberAsync(_66 string incomingPhoneNumber, string anonymousPhoneNumber)_66 {_66 var outgoingPhoneNumber = string.Empty;_66 var reservation = await _repository.FindByAnonymousPhoneNumberAsync(anonymousPhoneNumber);_66_66 // Connect from Guest to Host_66 if (reservation.Guest.PhoneNumber.Equals(incomingPhoneNumber))_66 {_66 outgoingPhoneNumber = reservation.Host.PhoneNumber;_66 }_66_66 // Connect from Host to Guest_66 if (reservation.Host.PhoneNumber.Equals(incomingPhoneNumber))_66 {_66 outgoingPhoneNumber = reservation.Guest.PhoneNumber;_66 }_66_66 return outgoingPhoneNumber;_66 }_66 }_66}
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 GatherOutgoingPhoneNumberAsync
helper method.
AirTNG.Web/Controllers/PhoneExchangeController.cs
_66using System.Threading.Tasks;_66using System.Web.Mvc;_66using AirTNG.Web.Models.Repository;_66using Twilio.TwiML;_66using Twilio.TwiML.Mvc;_66_66namespace AirTNG.Web.Controllers_66{_66 public class PhoneExchangeController : TwilioController_66 {_66 private readonly IReservationsRepository _repository;_66_66 public PhoneExchangeController() : this(new ReservationsRepository()) { }_66_66 public PhoneExchangeController(IReservationsRepository repository)_66 {_66 _repository = repository;_66 }_66_66 // POST: PhoneExchange/InterconnectUsingSms_66 [HttpPost]_66 public async Task<ActionResult> InterconnectUsingSms(string from, string to, string body)_66 {_66 var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);_66_66 var response = new TwilioResponse();_66 response.Message(body, new {to = outgoingPhoneNumber});_66_66 return TwiML(response);_66 }_66_66 // POST: PhoneExchange/InterconnectUsingVoice_66 [HttpPost]_66 public async Task<ActionResult> InterconnectUsingVoice(string from, string to)_66 {_66 var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);_66_66 var response = new TwilioResponse();_66 response.Play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3");_66 response.Dial(outgoingPhoneNumber);_66_66 return TwiML(response);_66 }_66_66 private async Task<string> GatherOutgoingPhoneNumberAsync(_66 string incomingPhoneNumber, string anonymousPhoneNumber)_66 {_66 var outgoingPhoneNumber = string.Empty;_66 var reservation = await _repository.FindByAnonymousPhoneNumberAsync(anonymousPhoneNumber);_66_66 // Connect from Guest to Host_66 if (reservation.Guest.PhoneNumber.Equals(incomingPhoneNumber))_66 {_66 outgoingPhoneNumber = reservation.Host.PhoneNumber;_66 }_66_66 // Connect from Host to Guest_66 if (reservation.Host.PhoneNumber.Equals(incomingPhoneNumber))_66 {_66 outgoingPhoneNumber = reservation.Guest.PhoneNumber;_66 }_66_66 return outgoingPhoneNumber;_66 }_66 }_66}
Let's see how to connect the guest and the host via phone call next.
Our Twilio application will send HTTP requests to this method on any incoming voice call. Our app responds with TwiML instructions that tell Twilio to Play
an introductory MP3 audio file and then Dial
either the guest or host, depending on who initiated the call.
AirTNG.Web/Controllers/PhoneExchangeController.cs
_66using System.Threading.Tasks;_66using System.Web.Mvc;_66using AirTNG.Web.Models.Repository;_66using Twilio.TwiML;_66using Twilio.TwiML.Mvc;_66_66namespace AirTNG.Web.Controllers_66{_66 public class PhoneExchangeController : TwilioController_66 {_66 private readonly IReservationsRepository _repository;_66_66 public PhoneExchangeController() : this(new ReservationsRepository()) { }_66_66 public PhoneExchangeController(IReservationsRepository repository)_66 {_66 _repository = repository;_66 }_66_66 // POST: PhoneExchange/InterconnectUsingSms_66 [HttpPost]_66 public async Task<ActionResult> InterconnectUsingSms(string from, string to, string body)_66 {_66 var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);_66_66 var response = new TwilioResponse();_66 response.Message(body, new {to = outgoingPhoneNumber});_66_66 return TwiML(response);_66 }_66_66 // POST: PhoneExchange/InterconnectUsingVoice_66 [HttpPost]_66 public async Task<ActionResult> InterconnectUsingVoice(string from, string to)_66 {_66 var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);_66_66 var response = new TwilioResponse();_66 response.Play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3");_66 response.Dial(outgoingPhoneNumber);_66_66 return TwiML(response);_66 }_66_66 private async Task<string> GatherOutgoingPhoneNumberAsync(_66 string incomingPhoneNumber, string anonymousPhoneNumber)_66 {_66 var outgoingPhoneNumber = string.Empty;_66 var reservation = await _repository.FindByAnonymousPhoneNumberAsync(anonymousPhoneNumber);_66_66 // Connect from Guest to Host_66 if (reservation.Guest.PhoneNumber.Equals(incomingPhoneNumber))_66 {_66 outgoingPhoneNumber = reservation.Host.PhoneNumber;_66 }_66_66 // Connect from Host to Guest_66 if (reservation.Host.PhoneNumber.Equals(incomingPhoneNumber))_66 {_66 outgoingPhoneNumber = reservation.Guest.PhoneNumber;_66 }_66_66 return outgoingPhoneNumber;_66 }_66 }_66}
That's it! We've just implemented anonymous communications that allow your customers to connect while protecting their privacy.
If you're a PHP developer working with Twilio, you might want to check out these other tutorials:
Save time and remove distractions by adding call screening and recording to your IVR (interactive voice response) system
Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages.
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.