Send Mass SMS Broadcasts in Ruby

October 21, 2014
Written by


Sometime in the next four weeks, my wife is going to go into labor. We’ll wait at home for as long as possible, then hop in the car and head to the hospital. At that point begins one of my comparatively few responsibilities over the next couple days: updating close friends and family of what’s going on.

Text messaging seems tailor made for times like these. Unfortunately, texting gets messy when it involves a group of people. To solve my message “delivery” problems, I’m building a group messaging app using Twilio SMS to broadcast one-to-many messages when I make an announcement, and receive one-to-one text-message when my friends text back. And now that Twilio supports MMS, I can send and receive pictures too.

Now, I’m building this app to make my life easier on baby day, but SMS broadcasting has tons more uses than this such as emergency management, teachers communicating with students, or talking trash with your fantasy football league to name a few.

In this tutorial, we’ll build an app to broadcast text messages using Sinatra, Google Docs, and Twilio. This app will:

  • Retrieve contact names and numbers from a Google Spreadsheet
  • Forward a message from your phone to all your contacts
  • Forward a message from one of your contacts to your phone

Here’s what you’ll need to get started:

The rest of this post will walk you through how to write this code line by line. If you’d like to jump to the finished product, check out my Text Message Broadcast GitHub repo. Also, at the bottom of this post you’ll find a Deploy to Heroku button that will get your own broadcast app live in minutes.

Retrieve contacts from a Google Spreadsheet

You could hard code your friends and family’s phone numbers, but that means re-deploying every time you want to add an aunt to the list. Who has time for that when there’s a baby on the way?

A while back I was inspired by this tweet by Patrick McKenzie, a longtime friend of Twilio who recently welcomed his own daughter into the world (Congrats Patrick!):

Screen Shot 2014-10-20 at 11.15.59 PM


Open Google Drive and create a new spreadsheet with two columns named ‘number’ and ‘name’ (it’s important that you give your columns these names and in this order — you’ll see why soon). Punch in a couple numbers to play with — when I was building my app, I used my cell phone and my wife’s cell phone. No point in spamming my friends during development.

Click File -> Publish to Web and you’ll see a link to a publicly accessible version of your document. Unfortunately the document at the other end of this link is not in JSON, but I found this article on How to Use Google Spreadsheet as a JSON backend that tells us how to fix that that.

In the middle of that link (and in the url in the navbar of your browser) is an ID for your spreadsheet:

You’re going to need that ID in a few minutes. First, open a terminal window and create a new directory for this project. In that directory create new file called broadcast.rb:
mkdir text-broadcast
cd text-broadcast
touch broadcast.rb

Open broadcast.rb and:

  • require the libraries needed to open a uri and parse json
  • define a constant for your spreadsheet ID
  • create a method to return the url of the spreadsheet data in JSON format

require 'json'
require 'open-uri'


def spreadsheet_url

Twilio expects ten digit phone numbers sans punctuation and spaces, prefixed with with a valid country code ( “+1” for the US). Let’s create a method that will add the country code and strip out non-digits. We’ll also strip off any leading ‘1’ so that we don’t end up with the country code on there twice.

Add this to the bottom of your code:

def sanitize(number)
  "+1" + number.gsub(/^1|\D/, "")

Next, create two methods to:

  • open the spreadsheet and parse the JSON data
  • iterate through each spreadsheet entry
  • extract the values for each number and name
  • return the pairs as a hash

def data_from_spreadsheet
  file = open(spreadsheet_url).read

def contacts_from_spreadsheet
  contacts = {}
  data_from_spreadsheet['feed']['entry'].each do |entry|
    name = entry['gsx$name']['$t']
    number = entry['gsx$number']['$t']
    contacts[sanitize(number)] = name

When run, contacts_from_spreadsheets returns a hash in the format of:

  '13178675309' => 'Dad', 
  '13175392342' => 'Mom'

For the sake of verbosity, create two simple methods to:

  • return an array of all of our contacts’ phone numbers
  • look up a contact’s name given their phone number

def contacts_numbers

def contact_name(number)

And that’s it for Google! If you want to give this a go, load this file in irb and try running each of the methods:

load './broadcast.rb'
puts contacts_from_spreadsheet
puts contacts_numbers
puts contact_name(contacts_numbers.first)

You’ve got an easy-to-maintain phonebook in the form of a Google Spreadsheet. Now let’s do something with those numbers.

Broadcast text messages using Sinatra and Twilio

One thing that’s surprised me since starting at Twilio is how useful Sinatra can be for building Twilio apps. I’ve always been more of a Rails guy, but it turns out that with Sinatra + Twilio you can build some pretty powerful apps in a single file.

To get started, create a Gemfile. It’s a simple app, so you won’t need much:

# Gemfile
source ''
ruby '2.1.2'
gem 'rack'
gem 'sinatra'
gem 'twilio-ruby'

Then install your gems from a terminal:

bundle install

Add two more requires to your broadcast.rb, then add a new constant for your cellphone number: 

require 'sinatra'
require 'twilio-ruby'


When your Twilio number receives a text, Twilio makes an HTTP request to your server. In the parameters of that request you can find details about the message in the same way that you would find data from from a form submission. Just as a web browser makes an HTTP request and expects an HTTP response in the form of HTML, Twilio expects a response in a form of Twilio-flavored XML that we call TwiML. For example, the generic TwiML to send a single message looks like this:

  <Message to='+13128675309' from='+1TWILIONUM'>
    <Body>Look at our new baby!!!</Body>

If you want to send multiple messages then you simply add more message blocks inside the response tag. If there’s no picture, you omit the tag.

With that roadmap in mind, create a method that will:

  • handle Twilio’s POST request
  • extract the message details
  • call yet-to-be-defined methods to generate instructions on where to forward the messages
  • return those instructions to Twilio as an XML response

post '/message' do
  from = params['From']
  body = params['Body']
  media_url = params['MediaUrl0']

  if from == MY_NUMBER
    twiml = send_to_contacts(body, media_url)
    twiml = send_to_me(from, body, media_url)

  content_type 'text/xml'

Now let’s create your send_to_contacts method which will:

  • accept a message body and (optional) media_url as arguments
  • iterate through your contacts’ numbers and…
  • … send a message to each number with the appropriate message body and media_url (unless it’s nil)
  • return the text of the generated TwiML response

Thanks to the Twilio Ruby gem, you won’t have to write TwiML by hand.

def send_to_contacts(body, media_url = nil)
  response = do |r|
    contacts_numbers.each do |num|
      r.Message to: num do |msg|
        msg.Body body
        msg.Media media_url unless media_url.nil?

Onto the second use case: a friend texts your Twilio number and you want to forward that message to your cell phone and your cell phone only. Now, when that message shows up on your phone it’s going to be from your Twilio number, so you’ll want to prepend the message body with the sender’s info.

def send_to_me(from, body, media_url = nil)
  name = contact_name(from)
  body = "#{name} (#{from}):\n#{body}"
  response = do |r|
    r.Message to: MY_NUMBER do |msg|
      msg.Body body
      msg.Media media_url unless media_url.nil?

And that’s it for the Ruby code! Open a terminal and fire up your Sinatra server:

ruby broadcast.rb

Make it Live

There are only two more steps needed to make your broadcast system operational from your development machine:

  • make your Sinatra server accessible to the public Internet
  • tell Twilio where to find it

There are a number of ways to do the former, but my favorite is to use ngrok. If you haven’t used ngrok before, this article from Kevin Whinnery will show you how to set it up in less than five minutes. I highly recommend registering for a free ngrok account so that you can take advantage of the subdomain flag which will prevent you from needing to update your webhook settings in Twilio everytime you restart ngrok. Once you’ve installed ngrok, start it up and point it at Sinatra’s default port, 4567:

ngrok -subdomain=example 4567

Finally, tell Twilio where to find your webserver when it gets an incoming text:


That’s it! Save your settings and send a text to your Twilio number. The numbers you defined in your spreadsheet should get a copy of the message. Reply from a number that’s not MY_NUMBER and you should get that message only on your cellphone. Now try it with a picture.

Deploy to Heroku

In order for this app to be useful when your computer’s offline, you’re going to have to get it off of your development machine and into the cloud. The easiest way to deploy the stock version of this app (found at my GitHub repo) is to click this button:


(Props to Heroku for that little piece of magic.)

Once you’ve done that, you’ll need set environment variables for MY_NUMBER and SPREADSHEET_ID:

  • go to the dashboard for your Heroku app
  • click Settings at the top right of the page
  • click Reveal Config Vars
  • click Edit
  • Add MY_NUMBER and your cellphone in the format of: +1XXXYYYZZZZ


Then, in your Twilio dashbaord, point the messaging URL for your Twilio number to 


Congrats! You now can blast out text and picture messages to friends and family while maintaining your contacts as easily as updating a spreadsheet. I hope it’s as useful for you as I imagine it will be for me in a few weeks.

If you run into any problems along the way, have a cool story to share about how you’ve used this app or if you have any tips on how to survive the first few weeks of parenthood, hit me up at or @greggyb. Fair warning though, I may be a bit preoccupied starting around mid-November.


Cover photo by: Claire SuniBaby by: Jarod Reyes.