This Ruby on Rails 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 guest's name and phone number.
app/controllers/reservations_controller.rb
1class ReservationsController < ApplicationController2skip_before_filter :verify_authenticity_token, only: [:accept_or_reject, :connect_guest_to_host_sms, :connect_guest_to_host_voice]3before_action :set_twilio_params, only: [:connect_guest_to_host_sms, :connect_guest_to_host_voice]4before_filter :authenticate_user, only: [:index]56# GET /reservations7def index8@reservations = current_user.reservations.all9end1011# GET /reservations/new12def new13@reservation = Reservation.new14end1516def create17@vacation_property = VacationProperty.find(params[:reservation][:property_id])18@reservation = @vacation_property.reservations.create(reservation_params)1920if @reservation.save21flash[:notice] = "Sending your reservation request now."22@reservation.host.check_for_reservations_pending23redirect_to @vacation_property24else25flash[:danger] = @reservation.errors26end27end2829# webhook for twilio incoming message from host30def accept_or_reject31incoming = params[:From]32sms_input = params[:Body].downcase33begin34@host = User.find_by(phone_number: incoming)35@reservation = @host.pending_reservation36if sms_input == "accept" || sms_input == "yes"37@reservation.confirm!38else39@reservation.reject!40end4142@host.check_for_reservations_pending4344sms_reponse = "You have successfully #{@reservation.status} the reservation."45respond(sms_reponse)46rescue Exception => e47puts "ERROR: #{e.message}"48sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."49respond(sms_reponse)50end51end5253# webhook for twilio to anonymously connect the two parties54def connect_guest_to_host_sms55# Guest -> Host56if @reservation.guest.phone_number == @incoming_phone57@outgoing_number = @reservation.host.phone_number5859# Host -> Guest60elsif @reservation.host.phone_number == @incoming_phone61@outgoing_number = @reservation.guest.phone_number62end6364response = Twilio::TwiML::MessagingResponse.new65response.message(:body => @message, :to => @outgoing_number)66render text: response.to_s67end6869# webhook for twilio -> TwiML for voice calls70def connect_guest_to_host_voice71# Guest -> Host72if @reservation.guest.phone_number == @incoming_phone73@outgoing_number = @reservation.host.phone_number7475# Host -> Guest76elsif @reservation.host.phone_number == @incoming_phone77@outgoing_number = @reservation.guest.phone_number78end79response = Twilio::TwiML::VoiceResponse.new80response.play(url: "http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")81response.dial(number: @outgoing_number)8283render text: response.to_s84end858687private88# Send an SMS back to the Subscriber89def respond(message)90response = Twilio::TwiML::MessagingResponse.new91response.message(body: message)9293render text: response.to_s94end9596# Never trust parameters from the scary internet, only allow the white list through.97def reservation_params98params.require(:reservation).permit(:name, :guest_phone, :message)99end100101# Load up Twilio parameters102def set_twilio_params103@incoming_phone = params[:From]104@message = params[:Body]105anonymous_phone_number = params[:To]106@reservation = Reservation.where(phone_number: anonymous_phone_number).first107end108109end
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 is still available. Learn how to automate this process in our first AirTNG tutorial, Workflow Automation.
Once the reservation is confirmed, we need to create a Twilio number that the guest and host can use to communicate in the provision_phone_number
method.
app/models/reservation.rb
1class Reservation < ActiveRecord::Base2validates :name, presence: true3validates :guest_phone, presence: true45enum status: [ :pending, :confirmed, :rejected ]67belongs_to :vacation_property8belongs_to :user910def notify_host(force = false)11# Don't send the message if we have more than one and we aren't being forced12if self.host.pending_reservations.length > 1 and !force13return14else15message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:1617'#{self.message}'1819Reply [accept] or [reject]."2021self.host.send_message_via_sms(message)22end23end2425def host26@host = User.find(self.vacation_property[:user_id])27end2829def guest30@guest = User.find_by(phone_number: self.guest_phone)31end3233def confirm!34provision_phone_number35self.update!(status: 1)36end3738def reject!39self.update!(status: 0)40end4142def notify_guest43if self.status_changed? && (self.status == :confirmed || self.status == :rejected)44message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."45self.guest.send_message_via_sms(message)46end47end4849def send_message_to_guest(message)50message = "From #{self.host.name}: #{message}"51self.guest.send_message_via_sms(message, self.phone_number)52end5354def send_message_to_host(message)55message = "From guest #{self.guest.name}: #{message}"56self.host.send_message_via_sms(message, self.phone_number)57end5859private6061def provision_phone_number62@client = Twilio::REST::Client.new(ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN'])63begin64# Lookup numbers in host area code, if none than lookup from anywhere65@numbers = @client.api.available_phone_numbers('US').local.list(area_code: self.host.area_code)66if @numbers.empty?67@numbers = @client.api.available_phone_numbers('US').local.list()68end6970# Purchase the number & set the application_sid for voice and sms, will71# tell the number where to route calls/sms72@number = @numbers.first.phone_number73@client.api.incoming_phone_numbers.create(74phone_number: @number,75voice_application_sid: ENV['ANONYMOUS_APPLICATION_SID'],76sms_application_sid: ENV['ANONYMOUS_APPLICATION_SID']77)7879# Set the reservation.phone_number80self.update!(phone_number: @number)8182rescue Exception => e83puts "ERROR: #{e.message}"84end85end86end
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 REST API Client 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.
app/models/reservation.rb
1class Reservation < ActiveRecord::Base2validates :name, presence: true3validates :guest_phone, presence: true45enum status: [ :pending, :confirmed, :rejected ]67belongs_to :vacation_property8belongs_to :user910def notify_host(force = false)11# Don't send the message if we have more than one and we aren't being forced12if self.host.pending_reservations.length > 1 and !force13return14else15message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:1617'#{self.message}'1819Reply [accept] or [reject]."2021self.host.send_message_via_sms(message)22end23end2425def host26@host = User.find(self.vacation_property[:user_id])27end2829def guest30@guest = User.find_by(phone_number: self.guest_phone)31end3233def confirm!34provision_phone_number35self.update!(status: 1)36end3738def reject!39self.update!(status: 0)40end4142def notify_guest43if self.status_changed? && (self.status == :confirmed || self.status == :rejected)44message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."45self.guest.send_message_via_sms(message)46end47end4849def send_message_to_guest(message)50message = "From #{self.host.name}: #{message}"51self.guest.send_message_via_sms(message, self.phone_number)52end5354def send_message_to_host(message)55message = "From guest #{self.guest.name}: #{message}"56self.host.send_message_via_sms(message, self.phone_number)57end5859private6061def provision_phone_number62@client = Twilio::REST::Client.new(ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN'])63begin64# Lookup numbers in host area code, if none than lookup from anywhere65@numbers = @client.api.available_phone_numbers('US').local.list(area_code: self.host.area_code)66if @numbers.empty?67@numbers = @client.api.available_phone_numbers('US').local.list()68end6970# Purchase the number & set the application_sid for voice and sms, will71# tell the number where to route calls/sms72@number = @numbers.first.phone_number73@client.api.incoming_phone_numbers.create(74phone_number: @number,75voice_application_sid: ENV['ANONYMOUS_APPLICATION_SID'],76sms_application_sid: ENV['ANONYMOUS_APPLICATION_SID']77)7879# Set the reservation.phone_number80self.update!(phone_number: @number)8182rescue Exception => e83puts "ERROR: #{e.message}"84end85end86end
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.
In our controller, we create a filter which gets executed every time Twilio asks our application how to handle an incoming call or text. This filter finds and stores the correct reservation (the one associated with the anonymous number) as an instance variable that will be used as we connect the guest and host via voice or SMS.
app/controllers/reservations_controller.rb
1class ReservationsController < ApplicationController2skip_before_filter :verify_authenticity_token, only: [:accept_or_reject, :connect_guest_to_host_sms, :connect_guest_to_host_voice]3before_action :set_twilio_params, only: [:connect_guest_to_host_sms, :connect_guest_to_host_voice]4before_filter :authenticate_user, only: [:index]56# GET /reservations7def index8@reservations = current_user.reservations.all9end1011# GET /reservations/new12def new13@reservation = Reservation.new14end1516def create17@vacation_property = VacationProperty.find(params[:reservation][:property_id])18@reservation = @vacation_property.reservations.create(reservation_params)1920if @reservation.save21flash[:notice] = "Sending your reservation request now."22@reservation.host.check_for_reservations_pending23redirect_to @vacation_property24else25flash[:danger] = @reservation.errors26end27end2829# webhook for twilio incoming message from host30def accept_or_reject31incoming = params[:From]32sms_input = params[:Body].downcase33begin34@host = User.find_by(phone_number: incoming)35@reservation = @host.pending_reservation36if sms_input == "accept" || sms_input == "yes"37@reservation.confirm!38else39@reservation.reject!40end4142@host.check_for_reservations_pending4344sms_reponse = "You have successfully #{@reservation.status} the reservation."45respond(sms_reponse)46rescue Exception => e47puts "ERROR: #{e.message}"48sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."49respond(sms_reponse)50end51end5253# webhook for twilio to anonymously connect the two parties54def connect_guest_to_host_sms55# Guest -> Host56if @reservation.guest.phone_number == @incoming_phone57@outgoing_number = @reservation.host.phone_number5859# Host -> Guest60elsif @reservation.host.phone_number == @incoming_phone61@outgoing_number = @reservation.guest.phone_number62end6364response = Twilio::TwiML::MessagingResponse.new65response.message(:body => @message, :to => @outgoing_number)66render text: response.to_s67end6869# webhook for twilio -> TwiML for voice calls70def connect_guest_to_host_voice71# Guest -> Host72if @reservation.guest.phone_number == @incoming_phone73@outgoing_number = @reservation.host.phone_number7475# Host -> Guest76elsif @reservation.host.phone_number == @incoming_phone77@outgoing_number = @reservation.guest.phone_number78end79response = Twilio::TwiML::VoiceResponse.new80response.play(url: "http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")81response.dial(number: @outgoing_number)8283render text: response.to_s84end858687private88# Send an SMS back to the Subscriber89def respond(message)90response = Twilio::TwiML::MessagingResponse.new91response.message(body: message)9293render text: response.to_s94end9596# Never trust parameters from the scary internet, only allow the white list through.97def reservation_params98params.require(:reservation).permit(:name, :guest_phone, :message)99end100101# Load up Twilio parameters102def set_twilio_params103@incoming_phone = params[:From]104@message = params[:Body]105anonymous_phone_number = params[:To]106@reservation = Reservation.where(phone_number: anonymous_phone_number).first107end108109end
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 made by the host, we forward it on to the guest. But if the message was sent by the guest, we forward it to the host.
app/controllers/reservations_controller.rb
1class ReservationsController < ApplicationController2skip_before_filter :verify_authenticity_token, only: [:accept_or_reject, :connect_guest_to_host_sms, :connect_guest_to_host_voice]3before_action :set_twilio_params, only: [:connect_guest_to_host_sms, :connect_guest_to_host_voice]4before_filter :authenticate_user, only: [:index]56# GET /reservations7def index8@reservations = current_user.reservations.all9end1011# GET /reservations/new12def new13@reservation = Reservation.new14end1516def create17@vacation_property = VacationProperty.find(params[:reservation][:property_id])18@reservation = @vacation_property.reservations.create(reservation_params)1920if @reservation.save21flash[:notice] = "Sending your reservation request now."22@reservation.host.check_for_reservations_pending23redirect_to @vacation_property24else25flash[:danger] = @reservation.errors26end27end2829# webhook for twilio incoming message from host30def accept_or_reject31incoming = params[:From]32sms_input = params[:Body].downcase33begin34@host = User.find_by(phone_number: incoming)35@reservation = @host.pending_reservation36if sms_input == "accept" || sms_input == "yes"37@reservation.confirm!38else39@reservation.reject!40end4142@host.check_for_reservations_pending4344sms_reponse = "You have successfully #{@reservation.status} the reservation."45respond(sms_reponse)46rescue Exception => e47puts "ERROR: #{e.message}"48sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."49respond(sms_reponse)50end51end5253# webhook for twilio to anonymously connect the two parties54def connect_guest_to_host_sms55# Guest -> Host56if @reservation.guest.phone_number == @incoming_phone57@outgoing_number = @reservation.host.phone_number5859# Host -> Guest60elsif @reservation.host.phone_number == @incoming_phone61@outgoing_number = @reservation.guest.phone_number62end6364response = Twilio::TwiML::MessagingResponse.new65response.message(:body => @message, :to => @outgoing_number)66render text: response.to_s67end6869# webhook for twilio -> TwiML for voice calls70def connect_guest_to_host_voice71# Guest -> Host72if @reservation.guest.phone_number == @incoming_phone73@outgoing_number = @reservation.host.phone_number7475# Host -> Guest76elsif @reservation.host.phone_number == @incoming_phone77@outgoing_number = @reservation.guest.phone_number78end79response = Twilio::TwiML::VoiceResponse.new80response.play(url: "http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")81response.dial(number: @outgoing_number)8283render text: response.to_s84end858687private88# Send an SMS back to the Subscriber89def respond(message)90response = Twilio::TwiML::MessagingResponse.new91response.message(body: message)9293render text: response.to_s94end9596# Never trust parameters from the scary internet, only allow the white list through.97def reservation_params98params.require(:reservation).permit(:name, :guest_phone, :message)99end100101# Load up Twilio parameters102def set_twilio_params103@incoming_phone = params[:From]104@message = params[:Body]105anonymous_phone_number = params[:To]106@reservation = Reservation.where(phone_number: anonymous_phone_number).first107end108109end
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.
app/controllers/reservations_controller.rb
1class ReservationsController < ApplicationController2skip_before_filter :verify_authenticity_token, only: [:accept_or_reject, :connect_guest_to_host_sms, :connect_guest_to_host_voice]3before_action :set_twilio_params, only: [:connect_guest_to_host_sms, :connect_guest_to_host_voice]4before_filter :authenticate_user, only: [:index]56# GET /reservations7def index8@reservations = current_user.reservations.all9end1011# GET /reservations/new12def new13@reservation = Reservation.new14end1516def create17@vacation_property = VacationProperty.find(params[:reservation][:property_id])18@reservation = @vacation_property.reservations.create(reservation_params)1920if @reservation.save21flash[:notice] = "Sending your reservation request now."22@reservation.host.check_for_reservations_pending23redirect_to @vacation_property24else25flash[:danger] = @reservation.errors26end27end2829# webhook for twilio incoming message from host30def accept_or_reject31incoming = params[:From]32sms_input = params[:Body].downcase33begin34@host = User.find_by(phone_number: incoming)35@reservation = @host.pending_reservation36if sms_input == "accept" || sms_input == "yes"37@reservation.confirm!38else39@reservation.reject!40end4142@host.check_for_reservations_pending4344sms_reponse = "You have successfully #{@reservation.status} the reservation."45respond(sms_reponse)46rescue Exception => e47puts "ERROR: #{e.message}"48sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."49respond(sms_reponse)50end51end5253# webhook for twilio to anonymously connect the two parties54def connect_guest_to_host_sms55# Guest -> Host56if @reservation.guest.phone_number == @incoming_phone57@outgoing_number = @reservation.host.phone_number5859# Host -> Guest60elsif @reservation.host.phone_number == @incoming_phone61@outgoing_number = @reservation.guest.phone_number62end6364response = Twilio::TwiML::MessagingResponse.new65response.message(:body => @message, :to => @outgoing_number)66render text: response.to_s67end6869# webhook for twilio -> TwiML for voice calls70def connect_guest_to_host_voice71# Guest -> Host72if @reservation.guest.phone_number == @incoming_phone73@outgoing_number = @reservation.host.phone_number7475# Host -> Guest76elsif @reservation.host.phone_number == @incoming_phone77@outgoing_number = @reservation.guest.phone_number78end79response = Twilio::TwiML::VoiceResponse.new80response.play(url: "http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")81response.dial(number: @outgoing_number)8283render text: response.to_s84end858687private88# Send an SMS back to the Subscriber89def respond(message)90response = Twilio::TwiML::MessagingResponse.new91response.message(body: message)9293render text: response.to_s94end9596# Never trust parameters from the scary internet, only allow the white list through.97def reservation_params98params.require(:reservation).permit(:name, :guest_phone, :message)99end100101# Load up Twilio parameters102def set_twilio_params103@incoming_phone = params[:From]104@message = params[:Body]105anonymous_phone_number = params[:To]106@reservation = Reservation.where(phone_number: anonymous_phone_number).first107end108109end
That's it! We've just implemented anonymous communications that allow your customers to connect while protecting their privacy with the help of the Twilio Ruby Helper Library.
If you're a Ruby developer working with Twilio, you might want to check out these other tutorials.
Part 1 of this Tutorial: Workflow Automation
Increase your rate of response by automating the workflows that are key to your business.
Send your customers a text message when they have an upcoming appointment - this tutorial shows you how to do it from a background job.
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.