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
_109class ReservationsController < ApplicationController_109 skip_before_filter :verify_authenticity_token, only: [:accept_or_reject, :connect_guest_to_host_sms, :connect_guest_to_host_voice]_109 before_action :set_twilio_params, only: [:connect_guest_to_host_sms, :connect_guest_to_host_voice]_109 before_filter :authenticate_user, only: [:index]_109_109 # GET /reservations_109 def index_109 @reservations = current_user.reservations.all_109 end_109_109 # GET /reservations/new_109 def new_109 @reservation = Reservation.new_109 end_109_109 def create_109 @vacation_property = VacationProperty.find(params[:reservation][:property_id])_109 @reservation = @vacation_property.reservations.create(reservation_params)_109_109 if @reservation.save_109 flash[:notice] = "Sending your reservation request now."_109 @reservation.host.check_for_reservations_pending_109 redirect_to @vacation_property_109 else_109 flash[:danger] = @reservation.errors_109 end_109 end_109_109 # webhook for twilio incoming message from host_109 def accept_or_reject_109 incoming = params[:From]_109 sms_input = params[:Body].downcase_109 begin_109 @host = User.find_by(phone_number: incoming)_109 @reservation = @host.pending_reservation_109 if sms_input == "accept" || sms_input == "yes"_109 @reservation.confirm!_109 else_109 @reservation.reject!_109 end_109_109 @host.check_for_reservations_pending_109_109 sms_reponse = "You have successfully #{@reservation.status} the reservation."_109 respond(sms_reponse)_109 rescue Exception => e_109 puts "ERROR: #{e.message}"_109 sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."_109 respond(sms_reponse)_109 end_109 end_109_109 # webhook for twilio to anonymously connect the two parties_109 def connect_guest_to_host_sms_109 # Guest -> Host_109 if @reservation.guest.phone_number == @incoming_phone_109 @outgoing_number = @reservation.host.phone_number_109_109 # Host -> Guest_109 elsif @reservation.host.phone_number == @incoming_phone_109 @outgoing_number = @reservation.guest.phone_number_109 end_109_109 response = Twilio::TwiML::MessagingResponse.new_109 response.message(:body => @message, :to => @outgoing_number)_109 render text: response.to_s_109 end_109_109 # webhook for twilio -> TwiML for voice calls_109 def connect_guest_to_host_voice_109 # Guest -> Host_109 if @reservation.guest.phone_number == @incoming_phone_109 @outgoing_number = @reservation.host.phone_number_109_109 # Host -> Guest_109 elsif @reservation.host.phone_number == @incoming_phone_109 @outgoing_number = @reservation.guest.phone_number_109 end_109 response = Twilio::TwiML::VoiceResponse.new_109 response.play(url: "http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")_109 response.dial(number: @outgoing_number)_109_109 render text: response.to_s_109 end_109_109_109 private_109 # Send an SMS back to the Subscriber_109 def respond(message)_109 response = Twilio::TwiML::MessagingResponse.new_109 response.message(body: message)_109_109 render text: response.to_s_109 end_109_109 # Never trust parameters from the scary internet, only allow the white list through._109 def reservation_params_109 params.require(:reservation).permit(:name, :guest_phone, :message)_109 end_109_109 # Load up Twilio parameters_109 def set_twilio_params_109 @incoming_phone = params[:From]_109 @message = params[:Body]_109 anonymous_phone_number = params[:To]_109 @reservation = Reservation.where(phone_number: anonymous_phone_number).first_109 end_109_109end
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
_86class Reservation < ActiveRecord::Base_86 validates :name, presence: true_86 validates :guest_phone, presence: true_86_86 enum status: [ :pending, :confirmed, :rejected ]_86_86 belongs_to :vacation_property_86 belongs_to :user_86_86 def notify_host(force = false)_86 # Don't send the message if we have more than one and we aren't being forced_86 if self.host.pending_reservations.length > 1 and !force_86 return_86 else_86 message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:_86_86 '#{self.message}'_86_86 Reply [accept] or [reject]."_86_86 self.host.send_message_via_sms(message)_86 end_86 end_86_86 def host_86 @host = User.find(self.vacation_property[:user_id])_86 end_86_86 def guest_86 @guest = User.find_by(phone_number: self.guest_phone)_86 end_86_86 def confirm!_86 provision_phone_number_86 self.update!(status: 1)_86 end_86_86 def reject!_86 self.update!(status: 0)_86 end_86_86 def notify_guest_86 if self.status_changed? && (self.status == :confirmed || self.status == :rejected)_86 message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."_86 self.guest.send_message_via_sms(message)_86 end_86 end_86_86 def send_message_to_guest(message)_86 message = "From #{self.host.name}: #{message}"_86 self.guest.send_message_via_sms(message, self.phone_number)_86 end_86_86 def send_message_to_host(message)_86 message = "From guest #{self.guest.name}: #{message}"_86 self.host.send_message_via_sms(message, self.phone_number)_86 end_86_86 private_86_86 def provision_phone_number_86 @client = Twilio::REST::Client.new(ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN'])_86 begin_86 # Lookup numbers in host area code, if none than lookup from anywhere_86 @numbers = @client.api.available_phone_numbers('US').local.list(area_code: self.host.area_code)_86 if @numbers.empty?_86 @numbers = @client.api.available_phone_numbers('US').local.list()_86 end_86_86 # Purchase the number & set the application_sid for voice and sms, will_86 # tell the number where to route calls/sms_86 @number = @numbers.first.phone_number_86 @client.api.incoming_phone_numbers.create(_86 phone_number: @number,_86 voice_application_sid: ENV['ANONYMOUS_APPLICATION_SID'],_86 sms_application_sid: ENV['ANONYMOUS_APPLICATION_SID']_86 )_86_86 # Set the reservation.phone_number_86 self.update!(phone_number: @number)_86_86 rescue Exception => e_86 puts "ERROR: #{e.message}"_86 end_86 end_86end
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
_86class Reservation < ActiveRecord::Base_86 validates :name, presence: true_86 validates :guest_phone, presence: true_86_86 enum status: [ :pending, :confirmed, :rejected ]_86_86 belongs_to :vacation_property_86 belongs_to :user_86_86 def notify_host(force = false)_86 # Don't send the message if we have more than one and we aren't being forced_86 if self.host.pending_reservations.length > 1 and !force_86 return_86 else_86 message = "You have a new reservation request from #{self.name} for #{self.vacation_property.description}:_86_86 '#{self.message}'_86_86 Reply [accept] or [reject]."_86_86 self.host.send_message_via_sms(message)_86 end_86 end_86_86 def host_86 @host = User.find(self.vacation_property[:user_id])_86 end_86_86 def guest_86 @guest = User.find_by(phone_number: self.guest_phone)_86 end_86_86 def confirm!_86 provision_phone_number_86 self.update!(status: 1)_86 end_86_86 def reject!_86 self.update!(status: 0)_86 end_86_86 def notify_guest_86 if self.status_changed? && (self.status == :confirmed || self.status == :rejected)_86 message = "Your recent request to stay at #{self.vacation_property.description} was #{self.status}."_86 self.guest.send_message_via_sms(message)_86 end_86 end_86_86 def send_message_to_guest(message)_86 message = "From #{self.host.name}: #{message}"_86 self.guest.send_message_via_sms(message, self.phone_number)_86 end_86_86 def send_message_to_host(message)_86 message = "From guest #{self.guest.name}: #{message}"_86 self.host.send_message_via_sms(message, self.phone_number)_86 end_86_86 private_86_86 def provision_phone_number_86 @client = Twilio::REST::Client.new(ENV['TWILIO_ACCOUNT_SID'], ENV['TWILIO_AUTH_TOKEN'])_86 begin_86 # Lookup numbers in host area code, if none than lookup from anywhere_86 @numbers = @client.api.available_phone_numbers('US').local.list(area_code: self.host.area_code)_86 if @numbers.empty?_86 @numbers = @client.api.available_phone_numbers('US').local.list()_86 end_86_86 # Purchase the number & set the application_sid for voice and sms, will_86 # tell the number where to route calls/sms_86 @number = @numbers.first.phone_number_86 @client.api.incoming_phone_numbers.create(_86 phone_number: @number,_86 voice_application_sid: ENV['ANONYMOUS_APPLICATION_SID'],_86 sms_application_sid: ENV['ANONYMOUS_APPLICATION_SID']_86 )_86_86 # Set the reservation.phone_number_86 self.update!(phone_number: @number)_86_86 rescue Exception => e_86 puts "ERROR: #{e.message}"_86 end_86 end_86end
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
_109class ReservationsController < ApplicationController_109 skip_before_filter :verify_authenticity_token, only: [:accept_or_reject, :connect_guest_to_host_sms, :connect_guest_to_host_voice]_109 before_action :set_twilio_params, only: [:connect_guest_to_host_sms, :connect_guest_to_host_voice]_109 before_filter :authenticate_user, only: [:index]_109_109 # GET /reservations_109 def index_109 @reservations = current_user.reservations.all_109 end_109_109 # GET /reservations/new_109 def new_109 @reservation = Reservation.new_109 end_109_109 def create_109 @vacation_property = VacationProperty.find(params[:reservation][:property_id])_109 @reservation = @vacation_property.reservations.create(reservation_params)_109_109 if @reservation.save_109 flash[:notice] = "Sending your reservation request now."_109 @reservation.host.check_for_reservations_pending_109 redirect_to @vacation_property_109 else_109 flash[:danger] = @reservation.errors_109 end_109 end_109_109 # webhook for twilio incoming message from host_109 def accept_or_reject_109 incoming = params[:From]_109 sms_input = params[:Body].downcase_109 begin_109 @host = User.find_by(phone_number: incoming)_109 @reservation = @host.pending_reservation_109 if sms_input == "accept" || sms_input == "yes"_109 @reservation.confirm!_109 else_109 @reservation.reject!_109 end_109_109 @host.check_for_reservations_pending_109_109 sms_reponse = "You have successfully #{@reservation.status} the reservation."_109 respond(sms_reponse)_109 rescue Exception => e_109 puts "ERROR: #{e.message}"_109 sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."_109 respond(sms_reponse)_109 end_109 end_109_109 # webhook for twilio to anonymously connect the two parties_109 def connect_guest_to_host_sms_109 # Guest -> Host_109 if @reservation.guest.phone_number == @incoming_phone_109 @outgoing_number = @reservation.host.phone_number_109_109 # Host -> Guest_109 elsif @reservation.host.phone_number == @incoming_phone_109 @outgoing_number = @reservation.guest.phone_number_109 end_109_109 response = Twilio::TwiML::MessagingResponse.new_109 response.message(:body => @message, :to => @outgoing_number)_109 render text: response.to_s_109 end_109_109 # webhook for twilio -> TwiML for voice calls_109 def connect_guest_to_host_voice_109 # Guest -> Host_109 if @reservation.guest.phone_number == @incoming_phone_109 @outgoing_number = @reservation.host.phone_number_109_109 # Host -> Guest_109 elsif @reservation.host.phone_number == @incoming_phone_109 @outgoing_number = @reservation.guest.phone_number_109 end_109 response = Twilio::TwiML::VoiceResponse.new_109 response.play(url: "http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")_109 response.dial(number: @outgoing_number)_109_109 render text: response.to_s_109 end_109_109_109 private_109 # Send an SMS back to the Subscriber_109 def respond(message)_109 response = Twilio::TwiML::MessagingResponse.new_109 response.message(body: message)_109_109 render text: response.to_s_109 end_109_109 # Never trust parameters from the scary internet, only allow the white list through._109 def reservation_params_109 params.require(:reservation).permit(:name, :guest_phone, :message)_109 end_109_109 # Load up Twilio parameters_109 def set_twilio_params_109 @incoming_phone = params[:From]_109 @message = params[:Body]_109 anonymous_phone_number = params[:To]_109 @reservation = Reservation.where(phone_number: anonymous_phone_number).first_109 end_109_109end
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
_109class ReservationsController < ApplicationController_109 skip_before_filter :verify_authenticity_token, only: [:accept_or_reject, :connect_guest_to_host_sms, :connect_guest_to_host_voice]_109 before_action :set_twilio_params, only: [:connect_guest_to_host_sms, :connect_guest_to_host_voice]_109 before_filter :authenticate_user, only: [:index]_109_109 # GET /reservations_109 def index_109 @reservations = current_user.reservations.all_109 end_109_109 # GET /reservations/new_109 def new_109 @reservation = Reservation.new_109 end_109_109 def create_109 @vacation_property = VacationProperty.find(params[:reservation][:property_id])_109 @reservation = @vacation_property.reservations.create(reservation_params)_109_109 if @reservation.save_109 flash[:notice] = "Sending your reservation request now."_109 @reservation.host.check_for_reservations_pending_109 redirect_to @vacation_property_109 else_109 flash[:danger] = @reservation.errors_109 end_109 end_109_109 # webhook for twilio incoming message from host_109 def accept_or_reject_109 incoming = params[:From]_109 sms_input = params[:Body].downcase_109 begin_109 @host = User.find_by(phone_number: incoming)_109 @reservation = @host.pending_reservation_109 if sms_input == "accept" || sms_input == "yes"_109 @reservation.confirm!_109 else_109 @reservation.reject!_109 end_109_109 @host.check_for_reservations_pending_109_109 sms_reponse = "You have successfully #{@reservation.status} the reservation."_109 respond(sms_reponse)_109 rescue Exception => e_109 puts "ERROR: #{e.message}"_109 sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."_109 respond(sms_reponse)_109 end_109 end_109_109 # webhook for twilio to anonymously connect the two parties_109 def connect_guest_to_host_sms_109 # Guest -> Host_109 if @reservation.guest.phone_number == @incoming_phone_109 @outgoing_number = @reservation.host.phone_number_109_109 # Host -> Guest_109 elsif @reservation.host.phone_number == @incoming_phone_109 @outgoing_number = @reservation.guest.phone_number_109 end_109_109 response = Twilio::TwiML::MessagingResponse.new_109 response.message(:body => @message, :to => @outgoing_number)_109 render text: response.to_s_109 end_109_109 # webhook for twilio -> TwiML for voice calls_109 def connect_guest_to_host_voice_109 # Guest -> Host_109 if @reservation.guest.phone_number == @incoming_phone_109 @outgoing_number = @reservation.host.phone_number_109_109 # Host -> Guest_109 elsif @reservation.host.phone_number == @incoming_phone_109 @outgoing_number = @reservation.guest.phone_number_109 end_109 response = Twilio::TwiML::VoiceResponse.new_109 response.play(url: "http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")_109 response.dial(number: @outgoing_number)_109_109 render text: response.to_s_109 end_109_109_109 private_109 # Send an SMS back to the Subscriber_109 def respond(message)_109 response = Twilio::TwiML::MessagingResponse.new_109 response.message(body: message)_109_109 render text: response.to_s_109 end_109_109 # Never trust parameters from the scary internet, only allow the white list through._109 def reservation_params_109 params.require(:reservation).permit(:name, :guest_phone, :message)_109 end_109_109 # Load up Twilio parameters_109 def set_twilio_params_109 @incoming_phone = params[:From]_109 @message = params[:Body]_109 anonymous_phone_number = params[:To]_109 @reservation = Reservation.where(phone_number: anonymous_phone_number).first_109 end_109_109end
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
_109class ReservationsController < ApplicationController_109 skip_before_filter :verify_authenticity_token, only: [:accept_or_reject, :connect_guest_to_host_sms, :connect_guest_to_host_voice]_109 before_action :set_twilio_params, only: [:connect_guest_to_host_sms, :connect_guest_to_host_voice]_109 before_filter :authenticate_user, only: [:index]_109_109 # GET /reservations_109 def index_109 @reservations = current_user.reservations.all_109 end_109_109 # GET /reservations/new_109 def new_109 @reservation = Reservation.new_109 end_109_109 def create_109 @vacation_property = VacationProperty.find(params[:reservation][:property_id])_109 @reservation = @vacation_property.reservations.create(reservation_params)_109_109 if @reservation.save_109 flash[:notice] = "Sending your reservation request now."_109 @reservation.host.check_for_reservations_pending_109 redirect_to @vacation_property_109 else_109 flash[:danger] = @reservation.errors_109 end_109 end_109_109 # webhook for twilio incoming message from host_109 def accept_or_reject_109 incoming = params[:From]_109 sms_input = params[:Body].downcase_109 begin_109 @host = User.find_by(phone_number: incoming)_109 @reservation = @host.pending_reservation_109 if sms_input == "accept" || sms_input == "yes"_109 @reservation.confirm!_109 else_109 @reservation.reject!_109 end_109_109 @host.check_for_reservations_pending_109_109 sms_reponse = "You have successfully #{@reservation.status} the reservation."_109 respond(sms_reponse)_109 rescue Exception => e_109 puts "ERROR: #{e.message}"_109 sms_reponse = "Sorry, it looks like you don't have any reservations to respond to."_109 respond(sms_reponse)_109 end_109 end_109_109 # webhook for twilio to anonymously connect the two parties_109 def connect_guest_to_host_sms_109 # Guest -> Host_109 if @reservation.guest.phone_number == @incoming_phone_109 @outgoing_number = @reservation.host.phone_number_109_109 # Host -> Guest_109 elsif @reservation.host.phone_number == @incoming_phone_109 @outgoing_number = @reservation.guest.phone_number_109 end_109_109 response = Twilio::TwiML::MessagingResponse.new_109 response.message(:body => @message, :to => @outgoing_number)_109 render text: response.to_s_109 end_109_109 # webhook for twilio -> TwiML for voice calls_109 def connect_guest_to_host_voice_109 # Guest -> Host_109 if @reservation.guest.phone_number == @incoming_phone_109 @outgoing_number = @reservation.host.phone_number_109_109 # Host -> Guest_109 elsif @reservation.host.phone_number == @incoming_phone_109 @outgoing_number = @reservation.guest.phone_number_109 end_109 response = Twilio::TwiML::VoiceResponse.new_109 response.play(url: "http://howtodocs.s3.amazonaws.com/howdy-tng.mp3")_109 response.dial(number: @outgoing_number)_109_109 render text: response.to_s_109 end_109_109_109 private_109 # Send an SMS back to the Subscriber_109 def respond(message)_109 response = Twilio::TwiML::MessagingResponse.new_109 response.message(body: message)_109_109 render text: response.to_s_109 end_109_109 # Never trust parameters from the scary internet, only allow the white list through._109 def reservation_params_109 params.require(:reservation).permit(:name, :guest_phone, :message)_109 end_109_109 # Load up Twilio parameters_109 def set_twilio_params_109 @incoming_phone = params[:From]_109 @message = params[:Body]_109 anonymous_phone_number = params[:To]_109 @reservation = Reservation.where(phone_number: anonymous_phone_number).first_109 end_109_109end
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.