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
1using System;2using System.Threading.Tasks;3using System.Web.Mvc;4using AirTNG.Web.Domain.PhoneNumber;5using AirTNG.Web.Domain.Reservations;6using AirTNG.Web.Models;7using AirTNG.Web.Models.Repository;8using AirTNG.Web.ViewModels;9using Microsoft.AspNet.Identity;10using Twilio.TwiML;11using Twilio.TwiML.Mvc;1213namespace AirTNG.Web.Controllers14{15[Authorize]16public class ReservationsController : TwilioController17{18private readonly IVacationPropertiesRepository _vacationPropertiesRepository;19private readonly IReservationsRepository _reservationsRepository;20private readonly IUsersRepository _usersRepository;21private readonly INotifier _notifier;22private readonly IPurchaser _phoneNumberPurchaser;2324public Func<string> UserId;2526public ReservationsController() : this(27new VacationPropertiesRepository(),28new ReservationsRepository(),29new UsersRepository(),30new Notifier(),31new Purchaser()) { }3233public ReservationsController(34IVacationPropertiesRepository vacationPropertiesRepository,35IReservationsRepository reservationsRepository,36IUsersRepository usersRepository,37INotifier notifier,38IPurchaser phoneNumberPurchaser)39{40_vacationPropertiesRepository = vacationPropertiesRepository;41_reservationsRepository = reservationsRepository;42_usersRepository = usersRepository;43_notifier = notifier;44_phoneNumberPurchaser = phoneNumberPurchaser;45UserId = () => User.Identity.GetUserId();46}4748public async Task<ActionResult> Index()49{50var user = await _usersRepository.FindAsync(UserId());51var reservations = user.Reservations;5253return View(reservations);54}5556// GET: Reservations/Create57public async Task<ActionResult> Create(int id)58{59var vacationProperty = await _vacationPropertiesRepository.FindAsync(id);60var reservation = new ReservationViewModel61{62ImageUrl = vacationProperty.ImageUrl,63Description = vacationProperty.Description,64VacationPropertyId = vacationProperty.Id,65VacationPropertyDescription = vacationProperty.Description,66UserName = vacationProperty.Owner.Name,67UserPhoneNumber = vacationProperty.Owner.PhoneNumber,68};6970return View(reservation);71}7273// POST: Reservations/Create74[HttpPost]75public async Task<ActionResult> Create(ReservationViewModel model)76{77if (ModelState.IsValid)78{79var reservation = new Reservation80{81Message = model.Message,82UserId = UserId(), // This is the reservee user ID83VactionPropertyId = model.VacationPropertyId,84Status = ReservationStatus.Pending,85CreatedAt = DateTime.Now86};8788await _reservationsRepository.CreateAsync(reservation);89await _reservationsRepository.LoadNavigationPropertiesAsync(reservation);9091await _notifier.SendNotificationAsync(reservation);9293return RedirectToAction("Index", "VacationProperties");94}9596return View(model);97}9899// POST Reservations/Handle100[HttpPost]101[AllowAnonymous]102public async Task<ActionResult> Handle(string from, string body)103{104string smsResponse;105106try107{108var host = await _usersRepository.FindByPhoneNumberAsync(from);109var reservation = await _reservationsRepository.FindFirstPendingReservationByHostAsync(host.Id);110111var smsRequest = body;112if (IsSmsRequestAccepted(smsRequest))113{114var purchasedPhoneNumber = _phoneNumberPurchaser.Purchase(host.AreaCode);115116reservation.Status = ReservationStatus.Confirmed;117reservation.AnonymousPhoneNumber = purchasedPhoneNumber.PhoneNumber;118}119else120{121reservation.Status = ReservationStatus.Rejected;122}123124await _reservationsRepository.UpdateAsync(reservation);125smsResponse =126string.Format("You have successfully {0} the reservation", reservation.Status);127}128catch (Exception)129{130smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";131}132133return TwiML(Respond(smsResponse));134}135136private static TwilioResponse Respond(string message)137{138var response = new TwilioResponse();139response.Message(message);140141return response;142}143144private static bool IsSmsRequestAccepted(string smsRequest)145{146return smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||147smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase);148}149}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
1using System;2using System.Threading.Tasks;3using System.Web.Mvc;4using AirTNG.Web.Domain.PhoneNumber;5using AirTNG.Web.Domain.Reservations;6using AirTNG.Web.Models;7using AirTNG.Web.Models.Repository;8using AirTNG.Web.ViewModels;9using Microsoft.AspNet.Identity;10using Twilio.TwiML;11using Twilio.TwiML.Mvc;1213namespace AirTNG.Web.Controllers14{15[Authorize]16public class ReservationsController : TwilioController17{18private readonly IVacationPropertiesRepository _vacationPropertiesRepository;19private readonly IReservationsRepository _reservationsRepository;20private readonly IUsersRepository _usersRepository;21private readonly INotifier _notifier;22private readonly IPurchaser _phoneNumberPurchaser;2324public Func<string> UserId;2526public ReservationsController() : this(27new VacationPropertiesRepository(),28new ReservationsRepository(),29new UsersRepository(),30new Notifier(),31new Purchaser()) { }3233public ReservationsController(34IVacationPropertiesRepository vacationPropertiesRepository,35IReservationsRepository reservationsRepository,36IUsersRepository usersRepository,37INotifier notifier,38IPurchaser phoneNumberPurchaser)39{40_vacationPropertiesRepository = vacationPropertiesRepository;41_reservationsRepository = reservationsRepository;42_usersRepository = usersRepository;43_notifier = notifier;44_phoneNumberPurchaser = phoneNumberPurchaser;45UserId = () => User.Identity.GetUserId();46}4748public async Task<ActionResult> Index()49{50var user = await _usersRepository.FindAsync(UserId());51var reservations = user.Reservations;5253return View(reservations);54}5556// GET: Reservations/Create57public async Task<ActionResult> Create(int id)58{59var vacationProperty = await _vacationPropertiesRepository.FindAsync(id);60var reservation = new ReservationViewModel61{62ImageUrl = vacationProperty.ImageUrl,63Description = vacationProperty.Description,64VacationPropertyId = vacationProperty.Id,65VacationPropertyDescription = vacationProperty.Description,66UserName = vacationProperty.Owner.Name,67UserPhoneNumber = vacationProperty.Owner.PhoneNumber,68};6970return View(reservation);71}7273// POST: Reservations/Create74[HttpPost]75public async Task<ActionResult> Create(ReservationViewModel model)76{77if (ModelState.IsValid)78{79var reservation = new Reservation80{81Message = model.Message,82UserId = UserId(), // This is the reservee user ID83VactionPropertyId = model.VacationPropertyId,84Status = ReservationStatus.Pending,85CreatedAt = DateTime.Now86};8788await _reservationsRepository.CreateAsync(reservation);89await _reservationsRepository.LoadNavigationPropertiesAsync(reservation);9091await _notifier.SendNotificationAsync(reservation);9293return RedirectToAction("Index", "VacationProperties");94}9596return View(model);97}9899// POST Reservations/Handle100[HttpPost]101[AllowAnonymous]102public async Task<ActionResult> Handle(string from, string body)103{104string smsResponse;105106try107{108var host = await _usersRepository.FindByPhoneNumberAsync(from);109var reservation = await _reservationsRepository.FindFirstPendingReservationByHostAsync(host.Id);110111var smsRequest = body;112if (IsSmsRequestAccepted(smsRequest))113{114var purchasedPhoneNumber = _phoneNumberPurchaser.Purchase(host.AreaCode);115116reservation.Status = ReservationStatus.Confirmed;117reservation.AnonymousPhoneNumber = purchasedPhoneNumber.PhoneNumber;118}119else120{121reservation.Status = ReservationStatus.Rejected;122}123124await _reservationsRepository.UpdateAsync(reservation);125smsResponse =126string.Format("You have successfully {0} the reservation", reservation.Status);127}128catch (Exception)129{130smsResponse = "Sorry, it looks like you don't have any reservations to respond to.";131}132133return TwiML(Respond(smsResponse));134}135136private static TwilioResponse Respond(string message)137{138var response = new TwilioResponse();139response.Message(message);140141return response;142}143144private static bool IsSmsRequestAccepted(string smsRequest)145{146return smsRequest.Equals("accept", StringComparison.InvariantCultureIgnoreCase) ||147smsRequest.Equals("yes", StringComparison.InvariantCultureIgnoreCase);148}149}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
1using System.Linq;2using AirTNG.Web.Domain.Twilio;3using Twilio;45namespace AirTNG.Web.Domain.PhoneNumber6{7public interface IPurchaser8{9IncomingPhoneNumber Purchase(string areaCode);10}1112public class Purchaser : IPurchaser13{14private readonly TwilioRestClient _client;1516public Purchaser() : this(new TwilioRestClient(Credentials.AccountSID, Credentials.AuthToken)) { }1718public Purchaser(TwilioRestClient client)19{20_client = client;21}2223/// <summary>24/// Purchase the first available phone number.25/// </summary>26/// <param name="areaCode">The area code</param>27/// <returns>The purchased phone number</returns>28public IncomingPhoneNumber Purchase(string areaCode)29{30var phoneNumberOptions = new PhoneNumberOptions31{32PhoneNumber = SearchForFirstAvailablePhoneNumber(areaCode),33VoiceApplicationSid = Credentials.ApplicationSID34};3536return _client.AddIncomingPhoneNumber(phoneNumberOptions);37}3839private string SearchForFirstAvailablePhoneNumber(string areaCode)40{41var searchParams = new AvailablePhoneNumberListRequest42{43AreaCode = areaCode,44VoiceEnabled = true,45SmsEnabled = true46};4748return _client49.ListAvailableLocalPhoneNumbers("US", searchParams)50.AvailablePhoneNumbers51.First() // We're only interested in the first available phone number.52.PhoneNumber;53}54}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
1using System.Threading.Tasks;2using System.Web.Mvc;3using AirTNG.Web.Models.Repository;4using Twilio.TwiML;5using Twilio.TwiML.Mvc;67namespace AirTNG.Web.Controllers8{9public class PhoneExchangeController : TwilioController10{11private readonly IReservationsRepository _repository;1213public PhoneExchangeController() : this(new ReservationsRepository()) { }1415public PhoneExchangeController(IReservationsRepository repository)16{17_repository = repository;18}1920// POST: PhoneExchange/InterconnectUsingSms21[HttpPost]22public async Task<ActionResult> InterconnectUsingSms(string from, string to, string body)23{24var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);2526var response = new TwilioResponse();27response.Message(body, new {to = outgoingPhoneNumber});2829return TwiML(response);30}3132// POST: PhoneExchange/InterconnectUsingVoice33[HttpPost]34public async Task<ActionResult> InterconnectUsingVoice(string from, string to)35{36var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);3738var response = new TwilioResponse();39response.Play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3");40response.Dial(outgoingPhoneNumber);4142return TwiML(response);43}4445private async Task<string> GatherOutgoingPhoneNumberAsync(46string incomingPhoneNumber, string anonymousPhoneNumber)47{48var outgoingPhoneNumber = string.Empty;49var reservation = await _repository.FindByAnonymousPhoneNumberAsync(anonymousPhoneNumber);5051// Connect from Guest to Host52if (reservation.Guest.PhoneNumber.Equals(incomingPhoneNumber))53{54outgoingPhoneNumber = reservation.Host.PhoneNumber;55}5657// Connect from Host to Guest58if (reservation.Host.PhoneNumber.Equals(incomingPhoneNumber))59{60outgoingPhoneNumber = reservation.Guest.PhoneNumber;61}6263return outgoingPhoneNumber;64}65}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
1using System.Threading.Tasks;2using System.Web.Mvc;3using AirTNG.Web.Models.Repository;4using Twilio.TwiML;5using Twilio.TwiML.Mvc;67namespace AirTNG.Web.Controllers8{9public class PhoneExchangeController : TwilioController10{11private readonly IReservationsRepository _repository;1213public PhoneExchangeController() : this(new ReservationsRepository()) { }1415public PhoneExchangeController(IReservationsRepository repository)16{17_repository = repository;18}1920// POST: PhoneExchange/InterconnectUsingSms21[HttpPost]22public async Task<ActionResult> InterconnectUsingSms(string from, string to, string body)23{24var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);2526var response = new TwilioResponse();27response.Message(body, new {to = outgoingPhoneNumber});2829return TwiML(response);30}3132// POST: PhoneExchange/InterconnectUsingVoice33[HttpPost]34public async Task<ActionResult> InterconnectUsingVoice(string from, string to)35{36var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);3738var response = new TwilioResponse();39response.Play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3");40response.Dial(outgoingPhoneNumber);4142return TwiML(response);43}4445private async Task<string> GatherOutgoingPhoneNumberAsync(46string incomingPhoneNumber, string anonymousPhoneNumber)47{48var outgoingPhoneNumber = string.Empty;49var reservation = await _repository.FindByAnonymousPhoneNumberAsync(anonymousPhoneNumber);5051// Connect from Guest to Host52if (reservation.Guest.PhoneNumber.Equals(incomingPhoneNumber))53{54outgoingPhoneNumber = reservation.Host.PhoneNumber;55}5657// Connect from Host to Guest58if (reservation.Host.PhoneNumber.Equals(incomingPhoneNumber))59{60outgoingPhoneNumber = reservation.Guest.PhoneNumber;61}6263return outgoingPhoneNumber;64}65}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
1using System.Threading.Tasks;2using System.Web.Mvc;3using AirTNG.Web.Models.Repository;4using Twilio.TwiML;5using Twilio.TwiML.Mvc;67namespace AirTNG.Web.Controllers8{9public class PhoneExchangeController : TwilioController10{11private readonly IReservationsRepository _repository;1213public PhoneExchangeController() : this(new ReservationsRepository()) { }1415public PhoneExchangeController(IReservationsRepository repository)16{17_repository = repository;18}1920// POST: PhoneExchange/InterconnectUsingSms21[HttpPost]22public async Task<ActionResult> InterconnectUsingSms(string from, string to, string body)23{24var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);2526var response = new TwilioResponse();27response.Message(body, new {to = outgoingPhoneNumber});2829return TwiML(response);30}3132// POST: PhoneExchange/InterconnectUsingVoice33[HttpPost]34public async Task<ActionResult> InterconnectUsingVoice(string from, string to)35{36var outgoingPhoneNumber = await GatherOutgoingPhoneNumberAsync(from, to);3738var response = new TwilioResponse();39response.Play("http://howtodocs.s3.amazonaws.com/howdy-tng.mp3");40response.Dial(outgoingPhoneNumber);4142return TwiML(response);43}4445private async Task<string> GatherOutgoingPhoneNumberAsync(46string incomingPhoneNumber, string anonymousPhoneNumber)47{48var outgoingPhoneNumber = string.Empty;49var reservation = await _repository.FindByAnonymousPhoneNumberAsync(anonymousPhoneNumber);5051// Connect from Guest to Host52if (reservation.Guest.PhoneNumber.Equals(incomingPhoneNumber))53{54outgoingPhoneNumber = reservation.Host.PhoneNumber;55}5657// Connect from Host to Guest58if (reservation.Host.PhoneNumber.Equals(incomingPhoneNumber))59{60outgoingPhoneNumber = reservation.Guest.PhoneNumber;61}6263return outgoingPhoneNumber;64}65}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.