Chatbots are programs that communicate some way with humans. They can be very basic, responding to keywords or phrases, or use something like Twilio Autopilot to take advantage of natural language understanding (NLU) to provide a richer experience and build out more complicated conversations.
In this tutorial we are going to see how easy it is to get started building chatbots for WhatsApp using the Twilio API for WhatsApp and the Ruby web framework Sinatra. Here's an example of the conversation we're going to build:
To build your own WhatsApp bot along with this tutorial, you will need the following:
- Ruby and Bundler installed
- ngrok so we can expose our local webhook endpoints to the world in style
- A WhatsApp account
- A Twilio account (if you don't have one, sign up for a new Twilio account here and receive $10 credit when you upgrade)
To launch a bot on WhatsApp you must go through an approval process with WhatsApp, but Twilio allows you to build and test your WhatsApp apps using our sandbox. Let's start by configuring the sandbox to use with your WhatsApp account.
The Twilio console walks you through the process, but here's what you need to do:
- Head to the WhatsApp sandbox area of the Twilio console, or navigate from the console to Programmable SMS and then WhatsApp
- The page will have the WhatsApp sandbox number on it. Open your WhatsApp application and start a new message to that number
- The page also has the message you need to send, which is "join" plus two random words, like "join flagrant-pigeon". Send your message to the sandbox number
When you receive a message back, you are all set up and ready to work with the sandbox.
Let's kick off a new Ruby application in which to build our bot. Start by creating a new directory to work in. Then initialise a new
Gemfile in the app and create a couple of files we'll need:
mkdir whatsapp-bot cd whatsapp-bot bundle init mkdir config touch bot.rb config.ru config/env.yml
Add the gems we will use to build this application:
- Sinatra, a simple web framework
- The twilio-ruby gem so we can generate TwiML
- http.rb to help us make some HTTP requests later
- Envyable to manage environment variables in the application
bundle add sinatra twilio-ruby http envyable
config/env.yml will store our application config and Envyable will load it into the environment for us. We only need to store one piece of config for this application: your Twilio auth token which you can find on your Twilio console dashboard. Add your auth token to
config.ru to load the application and the config, and to run it. Copy the following into
require 'bundler' Bundler.require Envyable.load('./config/env.yml') require './bot.rb' run WhatsAppBot
Let's test that everything is working as expected by creating a "Hello World!" Sinatra application. Open
bot.rb and enter the following code:
require "sinatra/base" class WhatsAppBot < Sinatra::Base get '/' do "Hello World!" end end
On the command line start the application with:
bundle exec rackup config.ru
The application will start on localhost:9292. Open that in your browser and you will see the text "Hello World!".
Now that our application is all set up we can start to build our bot. In this post we will build a simple bot that responds to two keywords when someone sends a message to our WhatsApp number. The words we're going to look for in the message are "dog" or "cat" and our bot will respond with a random picture and fact about either dogs or cats.
With the Twilio API for WhatsApp, when your number (or sandbox account) receives a message, Twilio makes a webhook request to a URL that you define. That request will include all the information about the message, including the body of the message.
Our application will need to define a route that we can set as the webhook request URL to receive those incoming messages, parse out whether the message contains the words we are looking for, and respond with the use of TwiML. TwiML is a set of XML elements that describe how your application communicates with Twilio.
The application we have built so far could respond to a webhook at the root path, but all it does is respond with "Hello World!" so let's get to work updating that.
Let's remove the "Hello World!" route and add a
/bot route instead. Twilio webhooks are
POST requests by default, so we'll set up the route to handle that too. To do so, we pass a block to the
post method that Sinatra defines.
require "sinatra/base" class WhatsAppBot < Sinatra::Base post '/bot' do end end
Next let's extract the body of the message from the request parameters. Since we're going to try to match the contents of the message against the words "dog" and "cat" we'll also translate the body to lower case.
class WhatsAppBot < Sinatra::Base post '/bot' do body = params["Body"].downcase end end
We are going to respond to the message using TwiML and the
twilio-ruby library gives us a useful class for building up our response:
Twilio::TwiML::MessagingResponse. Initialise a new response on the next line:
class WhatsAppBot < Sinatra::Base post '/bot' do body = params["Body"].downcase response = Twilio::TwiML::MessagingResponse.new end end
MessagingResponse object uses the builder pattern to generate the response. We're going to build a message and then add a body and media to it. We can pass a block to the
Twilio::TwiML::MessagingResponse#message method and that will nest those elements within a
<Message> element in the result.
class WhatsAppBot < Sinatra::Base post '/bot' do body = params["Body"].downcase response = Twilio::TwiML::MessagingResponse.new response.message do |message| # nested in a <Message> end end end
Now we need to start building our actual response. We'll check to see if the body includes the word "dog" or "cat" and add the relevant responses. If the body of the message contains neither word we should also add a default response to tell the user what the bot can respond to.
class WhatsAppBot < Sinatra::Base post '/bot' do body = params["Body"].downcase response = Twilio::TwiML::MessagingResponse.new response.message do |message| if body.include?("dog") # add dog fact and picture to the message end if body.include?("cat") # add cat fact and picture to the message end if !(body.include?("dog") || body.include?("cat")) message.body("I only know about dogs or cats, sorry!") end end end end
We currently have no way to get dog or cat facts. Luckily there are some APIs we can use for this. For dogs we will use the Dog CEO API for pictures and this dog API for facts. For cats there's TheCatAPI for pictures and the cat facts API for facts. We'll use the http.rb library we installed earlier to make requests to each of these APIs.
Each API works with
GET requests. To make a get request with http.rb you call
get on the
HTTP module passing the URL as a string. The
get method returns a response object whose contents you can read by calling
To make the application nice and tidy let's wrap up the API calls to each of these services into a
Cat module, each with a
Add these modules to the bottom
module Dog def self.fact response = HTTP.get("https://dog-api.kinduff.com/api/facts") JSON.parse(response.to_s)["facts"].first end def self.picture response = HTTP.get("https://dog.ceo/api/breeds/image/random") JSON.parse(response.to_s)["message"] end end module Cat def self.fact response = HTTP.get("https://catfact.ninja/fact") JSON.parse(response.to_s)["fact"] end def self.picture response = HTTP.get("https://api.thecatapi.com/v1/images/search") JSON.parse(response.to_s).first["url"] end end
Now we can use these modules in the webhook response like so:
class WhatsAppBot < Sinatra::Base post '/bot' do body = params["Body"].downcase response = Twilio::TwiML::MessagingResponse.new response.message do |message| if body.include?("dog") message.body(Dog.fact) message.media(Dog.picture) end if body.include?("cat") message.body(Cat.fact) message.media(Cat.picture) end if !(body.include?("dog") || body.include?("cat")) message.body("I only know about dogs or cats, sorry!") end end end end
To return the message back to WhatsApp via Twilio we need to set the content type of the response to "text/xml" and return the XML string.
class WhatsAppBot < Sinatra::Base post '/bot' do body = params["Body"].downcase response = Twilio::TwiML::MessagingResponse.new response.message do |message| if body.include?("dog") message.body(Dog.fact) message.media(Dog.picture) end if body.include?("cat") message.body(Cat.fact) message.media(Cat.picture) end if !(body.include?("dog") || body.include?("cat")) message.body("I only know about dogs or cats, sorry!") end end content_type "text/xml" response.to_xml end end
That's all the code for the webhook, but there's one more thing to consider.
This might not be the most mission critical data to be returning in a webhook request, but it is good practice to secure your webhooks to ensure you only respond to requests from the service you are expecting. Twilio signs all webhook requests using your auth token and you can validate that signature to validate the request.
twilio-ruby library provides rack middleware to make validating requests from Twilio easy: let's add that to the application too. At the top of your
WhatsAppBot class include the middleware with the
use method. Pass the following three arguments to
use: the middleware class
Rack::TwilioWebhookAuthentication, the auth token, and the path to protect (in this case, "/bot".)
class WhatsAppBot < Sinatra::Base use Rack::TwilioWebhookAuthentication, ENV['TWILIO_AUTH_TOKEN'], '/bot' post '/bot' do
On the command line stop the application with
ctrl/cmd + c and restart it with:
bundle exec rackup config.ru
We now need to make sure Twilio webhooks can reach our application. This is why I included ngrok in the requirements for this application. ngrok allows us to connect a public URL to the application running on our machine. If you don't already have ngrok installed, follow the instructions to download and install ngrok.
Start ngrok up to tunnel through to port 9292 with the following command:
ngrok http 9292
This will give you an ngrok URL that you can now add to your WhatsApp sandbox so that incoming messages will be directed to your application.
Take that ngrok URL and add the path to the bot so it looks like this:
https://YOUR_NGROK_SUBDOMAIN.ngrok.io/bot. Enter that URL in the WhatsApp sandbox admin in the input marked "When a message comes in" and save the configuration.
You can now send a message to the WhatsApp sandbox number and your application will swing into action to return you pictures and facts of dogs or cats.
In this post we've seen how to configure the Twilio API for WhatsApp and connect it up to a Ruby application to return pictures and facts of dogs or cats. You can get all the code for this bot on GitHub.
It's a simple bot, but provides a good basis to build more. You could look into receiving images from WhatsApp to make a visual bot or sending or receiving location as part of the message. We could also build on this to create even smarter bots using Twilio Autopilot.
Have you built any interesting bots? What other features would you like to see explored? Let me know in the comments or on Twitter at @philnash.