Building an Interactive Voice Response (IVR) System With Go and Twilio

December 23, 2024
Written by
Isijola Jeremiah
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Building an Interactive Voice Response (IVR) System with Go and Twilio

Interactive Voice Response (IVR) is a technology that allows users to interact with computer systems through voice and DTMF (Dual-Tone Multi-Frequency) tones using a telephone keypad. It serves as an automated telephony system that interacts with callers, gathers information, and routes calls to the appropriate recipient.

IVR systems offer a wide range of features designed to simplify communication processes and improve how things run. Some of these features include:

  • Automated Call Routing: Direct calls to the appropriate individual based on code input or predefined rules
  • Interactive Menus: Provide callers with menu options, such as "Press 1 for Sales" and "Press 2 for Support"
  • Data Collection: Gather and process caller information, such as account details or feedback
  • Integration with External Systems: Connect with databases, CRM systems, and other APIs to retrieve or update information
  • Multi-language Support: Offer support in multiple languages to cater to a global audience

This tutorial will guide you through the process of building an IVR system using Go and Twilio. Specifically, the system will provide weather information, where users will be able to contact your Twilio phone number to obtain current weather conditions and forecasts for their area.

Prerequisites

Before moving on, it is important to ensure you have the following:

  • A Twilio account (free or paid) with a Twilio phone number. If you're new, click here to create a free account.
  • Go 1.22 or higher installed on your machine
  • ngrok installed on your computer with an active account
  • A phone number to make voice calls to your application to test your IVR system

The project's workflow

We will set up an endpoint in the application that dynamically generates TwiML (Twilio Markup Language), enabling users to select a city and receive the weather information for that city.

This route will be made accessible to the public internet (despite being hosted on our local development machine) using ngrok. We will then configure our Twilio number in the Twilio console to post to this ngrok endpoint.

Set up the core of the project

To get started, let’s create the project folder for the application. Open your terminal and navigate to the directory where you want to create the project. Then, run the command below to create the application core directory:

mkdir go-twilio-ivr
cd go-twilio-ivr

Next, initialise a new Go module inside the directory by running the following command:

go mod init twilioIvr

In our IVR system, we will create and use three primary packages:

  • handler: For handling HTTP endpoints
  • models: Will contain a model for structuring our TwiML responses
  • utils: For supported city list and getting weather information

Now, let's create these packages by running the following commands:

mkdir handlers mkdir models mkdir utils

Next, create a new file named main.go in the root directory of your application. After creating the file, your project structure should look similar to the screenshot below:

Screenshot of Visual Studio Code showing GO-Twilio-IVR project with folders and main.go file selected.

Now, inside the main.go file add the following code.

package main  import (     "fmt"     "net/http"      "twilioIvr/handlers" )  func main() {     fmt.Println("server running. . . .. . ... .")      http.HandleFunc("POST /main_menu", handlers.MainMenuHandler)     http.HandleFunc("POST /handle_choice", handlers.HandleChoiceHandler)      http.ListenAndServe(":8083", nil) }

The code above creates a simple HTTP server using the built-in net/http package. The main() function:

  • Initialises the server
  • Defines routes for /main_menu and /handle_choice
  • Launches the server on port 8083

The handlers for these routes are specified in the handlers package, which we will develop in the next steps.

Build the IVR system

In this section, we will create the three packages we discussed: handlers, models, and utils. Each package will play a significant role in our IVR system. We will begin with the handlers package, which contains functions for handling HTTP requests and generating TwiML responses for Twilio.

Build HTTP handlers for TwiML response generation

Import the necessary packages

First, navigate to the handlers directory and create a handler.go file and add the necessary imports to it.

package handlers  import ( 	"encoding/xml" 	"fmt" 	"net/http"  	"twilioIvr/models" 	"twilioIvr/utils" )

Here, we imported the necessary packages and dependencies from twilioIvr/models and twilioIvr/utils to handle HTTP requests and generate TwiML responses for our IVR system.

Now, let's move on to the main part of the handlers package which is adding the MainMenuHandler function, and the HandleChoiceHandler() function which will present the caller with a list of available options to interact with the system.

Add the MainMenuHandler Function

Create a MainMenuHandler() function in the handler.go file by adding the following code to the end of handler .go:

func MainMenuHandler(w http.ResponseWriter, r *http.Request) { 	gather := &models.Gather{ 		Action:    "/handle_choice", 		Method:    "POST", 		NumDigits: 1, 		Say: &models.Say{ 			Text: "Welcome to the California Weather Tower Report. Press a digit to get the weather report for a specific city: " + 				"1 for Los Angeles, 2 for San Diego, 3 for San Jose, 4 for San Francisco, 5 for Fresno, 6 for Sacramento, " + 				"7 for Long Beach, 8 for Oakland, 9 for Bakersfield, 0 for Anaheim.", 		}, 	}  	response := &models.TwimlResponse{ 		Gather: gather, 	}
}

This code defines the MainMenuHandler() function, which creates a Gather object to collect a single-digit input from the caller, and presents a voice menu for selecting a city. Then, we create a new TwimlResponse object containing the Gather object to generate a TwiML response for the caller.

Next, let's proceed with handling the XML marshalling and response by adding the following code to the end of MainMenuHandler():

res, err := xml.Marshal(response) if err != nil { 	http.Error(w, err.Error(), http.StatusInternalServerError) 	return }  w.Header().Set("Content-Type", "application/xml") w.Write(res)

Marshalling the models.TwimlResponse struct into XML format using xml.Marshal() is essential. If an error occurs during marshalling, it writes an error message to the response with an HTTP 500 Internal Server Error code. If successful, it sets the response content type to application/xml and writes the marshalled XML data to the response body.

Add the HandleChoiceHandler Function

Next, we define the HandleChoiceHandler() function to manage user input and return appropriate weather reports. Paste the entire code, below, at the end of your handler.go file.

func HandleChoiceHandler(w http.ResponseWriter, r *http.Request) { 	err := r.ParseForm() 	if err != nil { 		http.Error(w, err.Error(), http.StatusInternalServerError) 		return 	}  	digit := r.FormValue("Digits") 	var response *models.TwimlResponse  	switch digit { 	case "1", "2", "3", "4", "5", "6", "7", "8", "9", "0": 		digitIndex := int(digit[0] - '0') 		if digitIndex == 0 { 			digitIndex = 10 // Map '0' to 10 for Anaheim 		}  		city := utils.Cities[digitIndex-1] 		weatherReport := utils.GetRandomWeatherReport() 		fullReport := fmt.Sprintf("The weather in %s: %s", city, weatherReport)  		response = &models.TwimlResponse{ 			Say: &models.Say{Text: fullReport, Voice: "woman", Language: "en-US"}, 		} 	default: 		response = &models.TwimlResponse{ 			Say: &models.Say{Text: "Sorry, I didn't catch that. Please try again.", Voice: "woman", Language: "en-US"}, 		} 	}  	res, err := xml.Marshal(response) 	if err != nil { 		http.Error(w, err.Error(), http.StatusInternalServerError) 		return 	}  	w.Header().Set("Content-Type", "application/xml") 	w.Write(res) }

Here is the breakdown of the code above. It:

  • Parses the form values from the request
  • Retrieves the digit input by the user from the form values
  • Processes different digit inputs using a switch case
  • Marshalls the models.TwimlResponse object into XML format and sends the appropriate weather report or error message

You can view and copy the full content of the handler.go file from this GitHub Gist.

Set up TwiML response structs

Now, create a model.go file inside the models folder. This file will define three structs used for building TwiML responses in your Go application. Inside the model.go file, add the following code:

package models  import "encoding/xml"  type TwimlResponse struct {     XMLName xml.Name `xml:"Response"`     Gather  *Gather  `xml:",omitempty"`     Say     *Say     `xml:",omitempty"` }  type Gather struct {     XMLName   xml.Name `xml:"Gather"`     Action    string   `xml:"action,attr"`     Method    string   `xml:"method,attr"`     NumDigits int      `xml:"numDigits,attr"`     Timeout   string   `xml:"timeout,attr,omitempty"`     Say       *Say     `xml:",omitempty"` }  type Say struct {     XMLName  xml.Name `xml:"Say"`     Text     string   `xml:",chardata"`     Voice    string   `xml:"voice,attr,omitempty"`     Language string   `xml:"language,attr,omitempty"` }

The import "encoding/xml" statement imports Go's encoding/xml package, which provides support for parsing and generating XML documents. This package is essential for converting Go structs into XML format and vice versa. In this context, it is used to generate TwiML responses, which are XML-based instructions for Twilio's APIs.

Here is an explanation of each struct and its fields:

  • The TwimlResponse struct, containing optional Gather and Say nested elements, serves as the root of a TwiML response.
  • The Gather struct collects keypad input with Action, Method, and NumDigits attributes, and an optional Timeout, including a message.
  • The Say struct plays a message to the user with customizable Text, Voice, and Language attributes.

Add utility functions for weather reports and city data

The next step involves creating a mock.db.go file in the utils directory. This file acts as a database of weather reports and city data. The utils package includes functions to provide random weather reports and city information.

These functions will be utilised later in the IVR flow to simulate responses for users who inquire about the weather in specific cities. They will call the GetCityWeather() function and provide an index corresponding to the city of interest. The function will then return a formatted string containing the city's name and a weather report. Add the following code to the mock.db.go file created earlier:

package utils  import ( 	"fmt" 	"math/rand" 	"time" )  var WeatherConditions = []string{ 	"Sunny, high of 75 degrees.", 	"Cloudy, high of 68 degrees.", 	"Partly cloudy, high of 72 degrees.", 	"Foggy, high of 64 degrees.", 	"Sunny, high of 80 degrees.", 	"Clear skies, high of 78 degrees.", 	"Breezy, high of 74 degrees.", 	"Mild, high of 70 degrees.", 	"Hot, high of 85 degrees.", 	"Pleasant, high of 76 degrees.", }  var Cities = []string{ 	"Los Angeles", 	"San Diego", 	"San Jose", 	"San Francisco", 	"Fresno", 	"Sacramento", 	"Long Beach", 	"Oakland", 	"Bakersfield", 	"Anaheim", }  func GetCityWeather(index int) string { 	if index < 0 || index >= len(Cities) { 		return "Invalid city index." 	}  	city := Cities[index] 	weatherReport := GetRandomWeatherReport() 	return fmt.Sprintf("The weather in %s: %s", city, weatherReport) }  func GetRandomWeatherReport() string { 	rand.NewSource(time.Now().UnixNano()) 	return WeatherConditions[rand.Intn(len(WeatherConditions))] }

This code defines two arrays: WeatherConditions and Cities. These store predefined weather reports and city names, respectively. The code also includes functions to retrieve weather reports for specific cities and to generate random weather reports.

Create a local tunnel with ngrok

Let's create a local tunnel to the application using ngrok, making our local server accessible over the internet. This is especially helpful for testing webhooks and integrating with services like Twilio which require access to a public URL for sending requests.

To create a tunnel, run the code below on your terminal:

ngrok http http://localhost:8083

This command will start ngrok and create a tunnel to your local server running on port 8083. You will see output similar to the screenshot below

Command prompt showing Ngrok session status, account, version, region, latency, web interface, and connections.

Update the webhook URL

Copy the Forwarding URL generated by ngrok. Then, open your Twilio Console, and navigate to Phone Numbers > Manage > Active numbers > <Your Twilio phone number> > Voice Configuration.

There, update the URL field next to the "A call comes in" dropdown with your ngrok Forwarding URL and append the URL with /main_menu (e.g., https://8d75-102-88-35-152.ngrok-free.app/main_menu), as in the screenshot below. After that, click Save configuration.

Twilio console showing voice configuration settings with a webhook URL highlighted.

With the local tunnel already set up, all that's left is to test our IVR system.

Test the IVR system

Then, open your terminal and run the command below in your application's top-level directory:

go run main.go

Next, place a call to your Twilio number and follow the instructions provided. Congratulations! You should now have your IVR system successfully communicating with you. You can find the entire code for this tutorial here.

That's how to build an Interactive Voice Response (IVR) system with Go and Twilio

Throughout this tutorial, we covered the step-by-step process of creating an IVR system using Go and Twilio. This project demonstrates the practical use of Go's fundamental principles such as structuring, managing HTTP requests, and XML conversion, in addition to Twilio's powerful communication APIs for building an interactive voice response system.

By following this guide, you've learned how to establish routes, handle user input, and generate dynamic responses, effectively merging Go and Twilio to construct a functional and interactive IVR application. You successfully built an IVR system!

Isijola Jeremiah is a developer who specialises in enhancing user experience on both the backend and frontend. Contact him on LinkedIn.

IVR icons created by wanicon - Flaticon.