ETA Notifications with Ruby and Rails

Download the Code

ETA Notifications

Companies like Uber, TaskRabbit, and Instacart have built an entire industry around the fact that we, the customers, like to order things instantly, wherever we are. The key to those services working? Notifying customers when things change.

Uber relies on Twilio SMS to keep customers up to date on their ridesharing requests. Learn more here.

In this tutorial, we'll build a notification system for a fake on-demand laundry service Laundr.io using Ruby on Rails.

Let's get started!  Click the below button to begin.

Trigger the Notifications

There are two cases we'd like to handle:

  1. Delivery person picks up laundry to be delivered ( /initial_notifications )
  2. Delivery person is arriving at the customer's house ( /delivery_notifications )

In a production app we would probably trigger the second notification when the delivery person was physically near the customer, using GPS.

(In this case we'll just use a button.)

Loading Code Samples...
Language
class OrdersController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :load_order, only: [:show, :send_initial_notification, :send_delivery_notification, :status]
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  def index
    @orders = Order.all
  end

  def show
  end

  # Endpoint for Twilio callback
  def status
    @order.notification_status = params["MessageStatus"]
    @order.save
    render nothing: true
  end

  def send_initial_notification
    @order.status = :shipped
    if @order.save
      message = 'Your laundry is done and on its way to you!'
      notify(message)
    else
      redirect_with_error
    end
  end

  def send_delivery_notification
    @order.status = :dropped_off
    if @order.save
      message = 'Your laundry is arriving now.'
      notify(message)
    else
      redirect_with_error
    end
  end

  private

  def notify(message)
    MessageSender.send_message(
      @order.id, request.host, @order.phone_number, message)
    redirect_to orders_url, notice: 'Message was delivered'
  end

  def redirect_with_error
    message = "An error has occurred updating the order status"
    redirect_to orders_url, flash: { error: message }
  end

  def load_order
    @order = Order.find(params[:id])
  end

  def record_not_found
    render 'shared/404', status: 404, layout: false
  end
end
app/controllers/orders_controller.rb
Trigger a customer notification

app/controllers/orders_controller.rb

Let's look at using the Ruby Twilio REST API Client to actually send out the notifications.

Set up the Twilio REST Client

Here we create a helper class with an authenticated Twilio REST API client that we can use anytime we need to send a text message.

We initialize it with our Twilio Account Credentials stored as environment variables.  You can find the Auth Token and Account SID in the console:

console credentials

Loading Code Samples...
Language
class MessageSender
  def self.send_message(order_id, host, to, message)
    new.send_message(order_id, host, to, message)
  end

  def initialize
    # To find TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN visit
    # https://www.twilio.com/console
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token  = ENV['TWILIO_AUTH_TOKEN']
    @client = Twilio::REST::Client.new(account_sid, auth_token)
  end

  def send_message(order_id, host, to, message)
    @client.messages.create(
      from:  twilio_number,
      to:    to,
      body:  message,
      status_callback: "http://#{host}/orders/#{order_id}/status"
    )
  end

  private

  def twilio_number
    # A Twilio number you control - choose one from:
    # https://www.twilio.com/console/phone-numbers/incoming
    # Specify in E.164 format, e.g. "+16519998877"
    ENV['TWILIO_NUMBER']
  end
end
lib/message_sender.rb
Set up the Twilio REST Client

lib/message_sender.rb

Next up: how we handle notification triggers.

Handle a Notification Trigger

This code handles the HTTP POST requests triggered by the delivery person.

It uses our MessageSender class to send an SMS message to the customer's phone number, which we have registered in our database.  Simple!

Loading Code Samples...
Language
class OrdersController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :load_order, only: [:show, :send_initial_notification, :send_delivery_notification, :status]
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  def index
    @orders = Order.all
  end

  def show
  end

  # Endpoint for Twilio callback
  def status
    @order.notification_status = params["MessageStatus"]
    @order.save
    render nothing: true
  end

  def send_initial_notification
    @order.status = :shipped
    if @order.save
      message = 'Your laundry is done and on its way to you!'
      notify(message)
    else
      redirect_with_error
    end
  end

  def send_delivery_notification
    @order.status = :dropped_off
    if @order.save
      message = 'Your laundry is arriving now.'
      notify(message)
    else
      redirect_with_error
    end
  end

  private

  def notify(message)
    MessageSender.send_message(
      @order.id, request.host, @order.phone_number, message)
    redirect_to orders_url, notice: 'Message was delivered'
  end

  def redirect_with_error
    message = "An error has occurred updating the order status"
    redirect_to orders_url, flash: { error: message }
  end

  def load_order
    @order = Order.find(params[:id])
  end

  def record_not_found
    render 'shared/404', status: 404, layout: false
  end
end
app/controllers/orders_controller.rb
Handle a notification trigger

app/controllers/orders_controller.rb

Next, let's look closer at how we push out the SMSes.

Send an SMS (or MMS)

Here we demonstrate how we actually send the SMS.

Picture worth 1,000 words?  We can add a picture of the laundry by adding:

media_url: "http://lorempixel.com/image_output/fashion-q-c-640-480-1.jpg"

In addition to the required parameters (and the optional media), we can pass a status_callback url to let us know if the message was delivered.

Loading Code Samples...
Language
class MessageSender
  def self.send_message(order_id, host, to, message)
    new.send_message(order_id, host, to, message)
  end

  def initialize
    # To find TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN visit
    # https://www.twilio.com/console
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token  = ENV['TWILIO_AUTH_TOKEN']
    @client = Twilio::REST::Client.new(account_sid, auth_token)
  end

  def send_message(order_id, host, to, message)
    @client.messages.create(
      from:  twilio_number,
      to:    to,
      body:  message,
      status_callback: "http://#{host}/orders/#{order_id}/status"
    )
  end

  private

  def twilio_number
    # A Twilio number you control - choose one from:
    # https://www.twilio.com/console/phone-numbers/incoming
    # Specify in E.164 format, e.g. "+16519998877"
    ENV['TWILIO_NUMBER']
  end
end
lib/message_sender.rb
Using Twilio's Ruby client to send an SMS

lib/message_sender.rb

Message status updates are interesting - let's look there next.

Handle a Callback from Twilio

Twilio will make a POST request to this controller each time our message status changes to one of the following: queued, failed, sent, delivered, or undelivered.

We then update this notification_status on the Order and let the business logic take over. This is a great place to add logic that would resend the message if it failed or send out an automated survey soon after a customer receives his or her clothes.

Loading Code Samples...
Language
class OrdersController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :load_order, only: [:show, :send_initial_notification, :send_delivery_notification, :status]
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  def index
    @orders = Order.all
  end

  def show
  end

  # Endpoint for Twilio callback
  def status
    @order.notification_status = params["MessageStatus"]
    @order.save
    render nothing: true
  end

  def send_initial_notification
    @order.status = :shipped
    if @order.save
      message = 'Your laundry is done and on its way to you!'
      notify(message)
    else
      redirect_with_error
    end
  end

  def send_delivery_notification
    @order.status = :dropped_off
    if @order.save
      message = 'Your laundry is arriving now.'
      notify(message)
    else
      redirect_with_error
    end
  end

  private

  def notify(message)
    MessageSender.send_message(
      @order.id, request.host, @order.phone_number, message)
    redirect_to orders_url, notice: 'Message was delivered'
  end

  def redirect_with_error
    message = "An error has occurred updating the order status"
    redirect_to orders_url, flash: { error: message }
  end

  def load_order
    @order = Order.find(params[:id])
  end

  def record_not_found
    render 'shared/404', status: 404, layout: false
  end
end
app/controllers/orders_controller.rb
Update Order notification_status after a Twilio Callback

app/controllers/orders_controller.rb

That's all, folks! We've just implemented an on-demand notification service that alerts our customers when their order is picked up or arriving.

Now let's look at other features that you might want to add to your application.

Where to next?

We've got lots of Ruby and Rails content here on the Docs site.  Sadly, we wanted to cut it down - here are just two other excellent tutorials you might enjoy:

 

Workflow Automation with Ruby and Rails

Increase your rate of response by automating the workflows that are key to your business. In this tutorial, learn how to build a ready-for-scale automated SMS workflow for a vacation rental company.

 

Masked Phone Numbers with Ruby and Rails

Protect your users' privacy by anonymously connecting them with Twilio Voice and SMS. Learn how to create disposable phone numbers on-demand so two users can communicate without exchanging personal information.

Did this help?

Thanks for checking this tutorial out! Let us know what you've built - or what you're building - on Twitter.

Mario Celi
Jose Oliveros
Paul Kamp
Andrew Baker
Agustin Camino

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
class OrdersController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :load_order, only: [:show, :send_initial_notification, :send_delivery_notification, :status]
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  def index
    @orders = Order.all
  end

  def show
  end

  # Endpoint for Twilio callback
  def status
    @order.notification_status = params["MessageStatus"]
    @order.save
    render nothing: true
  end

  def send_initial_notification
    @order.status = :shipped
    if @order.save
      message = 'Your laundry is done and on its way to you!'
      notify(message)
    else
      redirect_with_error
    end
  end

  def send_delivery_notification
    @order.status = :dropped_off
    if @order.save
      message = 'Your laundry is arriving now.'
      notify(message)
    else
      redirect_with_error
    end
  end

  private

  def notify(message)
    MessageSender.send_message(
      @order.id, request.host, @order.phone_number, message)
    redirect_to orders_url, notice: 'Message was delivered'
  end

  def redirect_with_error
    message = "An error has occurred updating the order status"
    redirect_to orders_url, flash: { error: message }
  end

  def load_order
    @order = Order.find(params[:id])
  end

  def record_not_found
    render 'shared/404', status: 404, layout: false
  end
end
class MessageSender
  def self.send_message(order_id, host, to, message)
    new.send_message(order_id, host, to, message)
  end

  def initialize
    # To find TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN visit
    # https://www.twilio.com/console
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token  = ENV['TWILIO_AUTH_TOKEN']
    @client = Twilio::REST::Client.new(account_sid, auth_token)
  end

  def send_message(order_id, host, to, message)
    @client.messages.create(
      from:  twilio_number,
      to:    to,
      body:  message,
      status_callback: "http://#{host}/orders/#{order_id}/status"
    )
  end

  private

  def twilio_number
    # A Twilio number you control - choose one from:
    # https://www.twilio.com/console/phone-numbers/incoming
    # Specify in E.164 format, e.g. "+16519998877"
    ENV['TWILIO_NUMBER']
  end
end
class OrdersController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :load_order, only: [:show, :send_initial_notification, :send_delivery_notification, :status]
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  def index
    @orders = Order.all
  end

  def show
  end

  # Endpoint for Twilio callback
  def status
    @order.notification_status = params["MessageStatus"]
    @order.save
    render nothing: true
  end

  def send_initial_notification
    @order.status = :shipped
    if @order.save
      message = 'Your laundry is done and on its way to you!'
      notify(message)
    else
      redirect_with_error
    end
  end

  def send_delivery_notification
    @order.status = :dropped_off
    if @order.save
      message = 'Your laundry is arriving now.'
      notify(message)
    else
      redirect_with_error
    end
  end

  private

  def notify(message)
    MessageSender.send_message(
      @order.id, request.host, @order.phone_number, message)
    redirect_to orders_url, notice: 'Message was delivered'
  end

  def redirect_with_error
    message = "An error has occurred updating the order status"
    redirect_to orders_url, flash: { error: message }
  end

  def load_order
    @order = Order.find(params[:id])
  end

  def record_not_found
    render 'shared/404', status: 404, layout: false
  end
end
class MessageSender
  def self.send_message(order_id, host, to, message)
    new.send_message(order_id, host, to, message)
  end

  def initialize
    # To find TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN visit
    # https://www.twilio.com/console
    account_sid = ENV['TWILIO_ACCOUNT_SID']
    auth_token  = ENV['TWILIO_AUTH_TOKEN']
    @client = Twilio::REST::Client.new(account_sid, auth_token)
  end

  def send_message(order_id, host, to, message)
    @client.messages.create(
      from:  twilio_number,
      to:    to,
      body:  message,
      status_callback: "http://#{host}/orders/#{order_id}/status"
    )
  end

  private

  def twilio_number
    # A Twilio number you control - choose one from:
    # https://www.twilio.com/console/phone-numbers/incoming
    # Specify in E.164 format, e.g. "+16519998877"
    ENV['TWILIO_NUMBER']
  end
end
class OrdersController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :load_order, only: [:show, :send_initial_notification, :send_delivery_notification, :status]
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

  def index
    @orders = Order.all
  end

  def show
  end

  # Endpoint for Twilio callback
  def status
    @order.notification_status = params["MessageStatus"]
    @order.save
    render nothing: true
  end

  def send_initial_notification
    @order.status = :shipped
    if @order.save
      message = 'Your laundry is done and on its way to you!'
      notify(message)
    else
      redirect_with_error
    end
  end

  def send_delivery_notification
    @order.status = :dropped_off
    if @order.save
      message = 'Your laundry is arriving now.'
      notify(message)
    else
      redirect_with_error
    end
  end

  private

  def notify(message)
    MessageSender.send_message(
      @order.id, request.host, @order.phone_number, message)
    redirect_to orders_url, notice: 'Message was delivered'
  end

  def redirect_with_error
    message = "An error has occurred updating the order status"
    redirect_to orders_url, flash: { error: message }
  end

  def load_order
    @order = Order.find(params[:id])
  end

  def record_not_found
    render 'shared/404', status: 404, layout: false
  end
end