How to Create a Voice Call Survey System with Twilio and Go

May 13, 2025
Written by
Popoola Temitope
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Create a Voice Call Survey System with Twilio and Go

Understanding how your customers feel about your services or products is crucial for making informed decisions to improve them. It helps you identify which ones interest your customers and which do not. This is why most companies collect feedback from customers after they have used their products or services.

When collecting survey data from customers, the most commonly used method is to send them a link to fill out their responses or provide feedback. While this method is sometimes effective, it can be time-consuming for customers to type their responses. However, with the voice call survey method, customers only need to respond to survey questions verbally, making it a more time-efficient option.

In this tutorial, you’ll learn how to create an interactive voice call survey system using Twilio and Go, as well as view the statistics of the collected survey responses.

Prerequisites

To follow along with this tutorial, you'll need the following:

Create a new Go project

To get started, let's initialize a new Go project. To do that, open your terminal, navigate to the desired project directory, and run the following commands.

mkdir voice-survey
cd voice-survey
go mod init voice-survey

After running the above commands, open your preferred code editor and open the new project folder.

Create environment variables for the credentials

Let’s create environment variables to store the application’s credentials, such as Twilio credentials and the ngrok base URL. To do this, in the project's root directory, create a .env file, and add the following environment variables.

TWILIO_ACCOUNT_SID=<wilio_account_sid>
TWILIO_AUTH_TOKEN=<twilio_auth_token>
TWILIO_FROM_NUMBER=<twilio_phone_number>
DB_SOURCE=<db_username>:<db_password>@tcp(localhost:3306)/voice_call_survey
BASE_URL=<forwarding_URL>

Now replace the <db_username> and <db_password> placeholders with your MySQL database username and password, respectively.

Retrieve your Twilio credentials

Now, let’s retrieve the Twilio credentials and set them as environment variables. Log in to your Twilio console dashboard, where you will find your Twilio credentials under the Account Info section, as shown in the screenshot below.

Twilio account info showing Account SID, hidden Auth Token, and phone number in a trial account.

In the application’s environment variables, replace the <twilio_account_sid>, <twilio_auth_token>, and <twilio_phone_number> placeholders with the corresponding Twilio Account SID, Auth Token, and phone number values.

Install Twilio's Go Helper Library

Twilio's Go Helper Library enables Go developers to seamlessly integrate Twilio services, such as sending SMS, making voice calls or other services, into applications built with the Go programming language.

To install it, run the command below in your terminal.

go get github.com/twilio/twilio-go

Install other dependencies

Let’s install the MySQL Go driver to interact with the MySQL database and the GoDotEnv library to load environment variables. To do this, run the following commands.

go get github.com/go-sql-driver/mysql github.com/joho/godotenv

Define the database schema

Let's create the "customers", "survey_responses", and "survey_questions" database tables to store the survey participants' details, the survey questions, and the collected survey responses, respectively.

To do that, log in to the MySQL database server and create a new database named "voice_call_survey". Then, create the tables using the following schema definition:

CREATE DATABASE voice_call_survey;

USE voice_call_survey;

CREATE TABLE customers (
    ID INT AUTO_INCREMENT PRIMARY KEY,
    phone VARCHAR(99) NOT NULL,
    CurrentQuestion VARCHAR(20) NOT NULL DEFAULT '1',
    surveyStatus VARCHAR(99) NOT NULL DEFAULT 'in_progress'
);

CREATE TABLE survey_questions (
    ID INT AUTO_INCREMENT PRIMARY KEY,
    Question VARCHAR(999) NOT NULL
);

CREATE TABLE survey_response (
    ID INT AUTO_INCREMENT PRIMARY KEY,
    questionID INT NOT NULL,
    respondentID INT NOT NULL,
    Answers VARCHAR(999) NOT NULL
);

The above schema consists of the following tables:

  • customers: Keeps track of participants' phone numbers and survey progress
  • survey_questions: Stores all survey questions
  • survey_response: Links responses to questions using questionID and tracks respondents via respondentID

Create the survey logic

Now, let’s create the voice survey logic, allowing the admin to add survey questions, invite customers to participate via phone call, and view statistics on the collected responses. To do this, navigate to the application's root directory, create a new file named main.go, and add the following code.

package main

import (
	"database/sql"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
	"strings"

	_ "github.com/go-sql-driver/mysql"
	"github.com/joho/godotenv"
	"github.com/twilio/twilio-go"
	openapi "github.com/twilio/twilio-go/rest/api/v2010"
)

var (
	baseURL string
	db      *sql.DB
)

type FormData struct {
	Phones    string
	Submitted bool
}

func initDB() {
	var err error
	db_source := os.Getenv("DB_SOURCE")
	db, err = sql.Open("mysql", db_source)
	if err != nil {
		log.Fatalf("Error connecting to database: %v", err)
	}
}

func placeCall(to string) error {
	client := twilio.NewRestClientWithParams(twilio.ClientParams{
		Username: os.Getenv("TWILIO_ACCOUNT_SID"),
		Password: os.Getenv("TWILIO_AUTH_TOKEN"),
	})
	twiml := fmt.Sprintf(`<Response>
		<Gather input="speech" action="%s/handle-response" method="POST" speechTimeout="auto" language="en-US">
			<Say>Hello, do you want to take our survey? Please say Yes or No.</Say>
		</Gather>
	</Response>`, baseURL)
	params := &openapi.CreateCallParams{}
	params.SetTo(to)
	params.SetFrom(os.Getenv("TWILIO_FROM_NUMBER"))
	params.SetTwiml(twiml)
	_, err := client.Api.CreateCall(params)
	if err != nil {
		return fmt.Errorf("error placing call to %s: %v", to, err)
	}

	log.Printf("Call placed successfully to %s\n", to)
	return nil
}

func handleResponse(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Failed to parse response", http.StatusBadRequest)
		return
	}
	speechText := r.FormValue("SpeechResult")
	phone := r.FormValue("To")
	log.Println("User said:", speechText, "To", phone)
	response := strings.ToLower(strings.TrimSpace(speechText))

	if strings.Contains(response, "yes") {
		_, err := db.Exec("INSERT INTO customers (phone, CurrentQuestion, surveyStatus) VALUES (?, ?, ?)", phone, 1, "in_progress")
		if err != nil {
			log.Printf("Failed to save user agreement: %v\n", err)
		}
		var firstQuestion string
		err = db.QueryRow("SELECT Question FROM survey_questions WHERE ID = 1").Scan(&firstQuestion)
		if err != nil {
			log.Printf("Failed to fetch first question: %v\n", err)
			firstQuestion = "We could not fetch the first question. Please try again later."
		}

		fmt.Fprintf(w, `<Response>
			<Gather input="speech" action="%s/handle-survey" method="POST" speechTimeout="auto" language="en-US">
				<Say>%s</Say>
			</Gather>
		</Response>`, baseURL, firstQuestion)
	} else {
		fmt.Fprintln(w, `<Response><Say>Thank you for your time. Goodbye!</Say></Response>`)
	}
}

func handleSurvey(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Failed to parse response", http.StatusBadRequest)
		return
	}
	phone := r.FormValue("To")
	answer := r.FormValue("SpeechResult")
	var respondentID int
	err := db.QueryRow("SELECT ID FROM customers WHERE phone = ?", phone).Scan(&respondentID)
	if err != nil {
		http.Error(w, "Survey error", http.StatusInternalServerError)
		return
	}
	
	var questionID int
	err = db.QueryRow("SELECT CurrentQuestion FROM customers WHERE ID = ?", respondentID).Scan(&questionID)
	if err != nil {
		http.Error(w, "Survey error", http.StatusInternalServerError)
		return
	}

	_, err = db.Exec("INSERT INTO survey_response (questionID, respondentID, Answers) VALUES (?, ?, ?)", questionID, respondentID, answer)
	if err != nil {
		log.Printf("Failed to save response: %v\n", err)
	}

	var nextQuestion string
	err = db.QueryRow("SELECT Question FROM survey_questions WHERE ID = ?", questionID+1).Scan(&nextQuestion)
	if err != nil {
		fmt.Fprintln(w, `<Response><Say>Thank you for completing the survey. Goodbye!</Say></Response>`)
		_, err = db.Exec("UPDATE customers SET surveyStatus = ? WHERE ID = ?", "completed", respondentID)
		if err != nil {
			log.Printf("Failed to update survey status: %v\n", err)
		}
		return
	}

	_, err = db.Exec("UPDATE customers SET CurrentQuestion = ? WHERE ID = ?", questionID+1, respondentID)
	if err != nil {
		log.Printf("Failed to update current question: %v\n", err)
	}
	fmt.Fprintf(w, `<Response>
		<Gather input="speech" action="%s/handle-survey" method="POST" speechTimeout="auto" language="en-US">
			<Say>%s</Say>
		</Gather>
	</Response>`, baseURL, nextQuestion)
}

func serveForm(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("templates/invite.html")
	if err != nil {
		http.Error(w, "Template not found", http.StatusInternalServerError)
		return
	}

	data := FormData{}
	if r.Method == http.MethodPost {
		if err := r.ParseForm(); err != nil {
			http.Error(w, "Failed to parse form", http.StatusBadRequest)
			return
		}
		data.Phones = r.FormValue("phones")
		data.Submitted = true
		phoneNumbers := strings.Split(data.Phones, "\n")
		for _, number := range phoneNumbers {
			number = strings.TrimSpace(number)
			if number != "" {
				if err := placeCall(number); err != nil {
					log.Printf("Failed to place call to %s: %v\n", number, err)
				}
			}
		}
	}
	tmpl.Execute(w, data)
}

func addQuestion(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodPost {
		if err := r.ParseForm(); err != nil {
			http.Error(w, "Failed to parse form", http.StatusBadRequest)
			return
		}
		question := r.FormValue("question")
		if question == "" {
			http.Error(w, "Question cannot be empty", http.StatusBadRequest)
			return
		}
		_, err := db.Exec("INSERT INTO survey_questions (Question) VALUES (?)", question)
		if err != nil {
			http.Error(w, "Database error", http.StatusInternalServerError)
			return
		}
		http.Redirect(w, r, "/add-question", http.StatusSeeOther)
		return
	}

	rows, err := db.Query("SELECT Question FROM survey_questions")
	if err != nil {
		http.Error(w, "Database error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var questions []string
	for rows.Next() {
		var question string
		if err := rows.Scan(&question); err != nil {
			continue
		}
		questions = append(questions, question)
	}

	tmpl, err := template.ParseFiles("templates/add_question.html")
	if err != nil {
		http.Error(w, "Template not found", http.StatusInternalServerError)
		return
	}
	tmpl.Execute(w, questions)
}

func surveyStats(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.ParseFiles("templates/survey_stats.html")
	if err != nil {
		http.Error(w, "Template not found", http.StatusInternalServerError)
		return
	}

	var totalParticipants int
	err = db.QueryRow("SELECT COUNT(*) FROM customers").Scan(&totalParticipants)
	if err != nil {
		http.Error(w, "Database error", http.StatusInternalServerError)
		return
	}

	var completedSurveys int
	err = db.QueryRow("SELECT COUNT(*) FROM customers WHERE surveyStatus = 'completed'").Scan(&completedSurveys)
	if err != nil {
		http.Error(w, "Database error", http.StatusInternalServerError)
		return
	}
	query := `
		SELECT sq.Question, sr.Answers, COUNT(*) 
		FROM survey_response sr
		JOIN survey_questions sq ON sr.questionID = sq.ID
		GROUP BY sq.Question, sr.Answers;
	`
	rows, err := db.Query(query)
	if err != nil {
		http.Error(w, "Database error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	type ResponseData struct {
		Question string
		Response string
		Count    int
	}
	var responseStats []ResponseData
	for rows.Next() {
		var data ResponseData
		if err := rows.Scan(&data.Question, &data.Response, &data.Count); err != nil {
			continue
		}
		responseStats = append(responseStats, data)
	}
	if err := rows.Err(); err != nil {
		http.Error(w, "Database error", http.StatusInternalServerError)
		return
	}
	
	data := struct {
		TotalParticipants int
		CompletedSurveys  int
		ResponseStats     []ResponseData
	}{
		TotalParticipants: totalParticipants,
		CompletedSurveys:  completedSurveys,
		ResponseStats:     responseStats,
	}
	err = tmpl.Execute(w, data)
	if err != nil {
		http.Error(w, "Template rendering error", http.StatusInternalServerError)
	}
}

func main() {
	godotenv.Load()
	baseURL = os.Getenv("BASE_URL")
	
	initDB()
	
	http.HandleFunc("/", serveForm)
	http.HandleFunc("/handle-response", handleResponse)
	http.HandleFunc("/handle-survey", handleSurvey)
	http.HandleFunc("/add-question", addQuestion)
	http.HandleFunc("/survey-stats", surveyStats)

	log.Println("Server started at http://localhost:8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatalf("Server failed: %v", err)
	}
}

In the code above:

  • All the necessary dependencies are imported.
  • The initDB() function establishes a connection to the database server using the sql.Open() method, which accepts two parameters: mysql, specifying the database driver, and db_source, which contains the database connection string loaded from the environment variable.
  • The placeCall() function initializes a Twilio client using the twilio.NewRestClientWithParams() method, which accepts the Twilio Account SID and Auth Token. The <Gather> verb is used in the TwiML response to collect voice input from customers. Finally, the client.Api.CreateCall() method is used to place a call to the customer's phone number, asking if they would like to answer the survey questions.
  • The handleResponse() function processes the participant's consent to take the survey by capturing user responses through the Twilio Voice webhook. If the participant agrees by saying "yes", their phone number is stored in the database and the first survey question is asked.
  • The handleSurvey() function gets the customer’s response through the Twilio Voice webhook, stores the answer in the database, and then processes the next questions.
  • The addQuestion() function adds new survey questions to the database, while the surveyStats() function retrieves collected customer responses, enabling the admin to make informed decisions.
  • The main() function serves as the entry point of the application. It loads environment variables, initializes the database connection using initDB(), and sets up the application routes before starting the HTTP server.

Create the application templates

Let’s create the user interface templates for the "add_questions", "invite", and "survey_stats" pages. From the application's root directory, create a folder named templates.

To create the "add_question" interface, navigate to the templates folder, create a file named add_question.html, and add the following code.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Add Survey Question</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container py-4">
    <div class="card shadow-sm p-4">
        <h2 class="mb-3">Add New Survey Question</h2>
        <form action="/add-question" method="POST" class="mb-4">
            <div class="mb-3">
                <input type="text" name="question" class="form-control" placeholder="Enter your question" required>
            </div>
            <button type="submit" class="btn btn-primary">Add Question</button>
        </form>
    </div>
    <div class="mt-4">
        <h3>Existing Questions</h3>
        <ul class="list-group">
            {{range .}}
            <li class="list-group-item">{{.}}</li>
            {{end}}
        </ul>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Next, to create the "invite page" interface, create a new file named invite.html and add the following code.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Invite Customer</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="d-flex justify-content-center align-items-center vh-100 bg-light">
    <div class="container bg-white p-4 rounded shadow" style="max-width: 400px;">
        <h2 class="text-center mb-3">Invite Customer</h2>
        <form action="/" method="post">
            <div class="mb-3">
                <label for="phones" class="form-label">Customer Numbers:</label>
                <textarea class="form-control" name="phones" id="phones" rows="4" placeholder="Enter one number per line"></textarea>
            </div>
            <button type="submit" class="btn btn-success w-100">Submit</button>
        </form>
        {{if .Submitted}}
        <div class="mt-4 p-3 border rounded bg-light">
            <h4 class="text-center">Submitted Data</h4>
            <p><strong>Customer Numbers:</strong></p>
            <pre class="bg-white p-2 border rounded">{{.Phones}}</pre>
        </div>
        {{end}}
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

And, to create the "survey_stats page" interface, create a new file named survey_stats.html and add the following code to it.

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Survey Statistics</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
</head>
<body class="container mt-4">
    <h2 class="mb-4">Survey Statistics</h2>
    <div class="card p-3">
        <p><strong>Total Participants:</strong> {{.TotalParticipants}}</p>
        <p><strong>Surveys Completed:</strong> {{.CompletedSurveys}}</p>
    </div>
    <h3 class="mt-4">Response Distribution</h3>
    <table class="table table-bordered mt-3">
        <thead>
            <tr>
                <th>Question</th>
                <th>Response</th>
                <th>Count</th>
            </tr>
        </thead>
        <tbody>
            {{range .ResponseStats}}
            <tr>
                <td>{{.Question}}</td>
                <td>{{.Response}}</td>
                <td>{{.Count}}</td>
            </tr>
            {{end}}
        </tbody>
    </table>
</body>
</html>

Make the application accessible on the internet

Finally, to enable the application to process survey voice responses, it must be accessible over the internet. You can use ngrok to achieve this. To do so, run the following command in your terminal.

ngrok http http://localhost:8080

The command will generate a Forwarding URL in the terminal output, as shown in the screenshot below.

Next, let’s add the ForwardingURL to the environment variables. To do this, open the .env file and replace the <forwarding_url> placeholder with the generated Forwarding URL.

Test the application

Now, let’s test the voice call survey application. To do this, start the application development server. Open another terminal tab or session and run the command below.

go run main.go

Next, to add survey questions, open http://localhost:8080/add-question in your browser and enter your questions, as shown in the screenshot below.

To invite customers to the voice call survey, open http://localhost:8080/invite in your browser and enter the participants' phone numbers as shown in the screenshot below.

After inviting the participants, they will receive a call to answer the survey questions. After one or more have answered the survey, you can view the survey statistics by opening http://localhost:8080/survey-stats in your browser, as shown in the screenshot below.

That’s how to create a voice call survey system with Twilio and Go

Surveys help businesses identify which services or products to invest in, enhance, or discontinue by providing valuable insights into customer preferences and feedback.

In this tutorial, you learned how to create an interactive voice call survey system and analyze the collected survey responses statistics using Twilio and Go.

Popoola Temitope is a mobile developer and a technical writer who loves writing about frontend technologies. He can be reached on LinkedIn.

Survey icons created by Freepik and Phone icons created by Kalashnyk on Flaticon.