How to Build an IVR with Django and Programmable Voice

June 07, 2024
Written by
Chepkirui Dorothy
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Diane Phan
Twilion

In this guide, you will use the Twilio Programmable Voice API to build an interactive voice response (IVR) for an imaginary pizza shop. This automated telephony system will use touch-tone keypad selections to guide users through pizza menu options. The menu options and responses will have been stored in an SQL database and managed by Django through its ORM .

By the end of this article, you will learn how to integrate Twilio Programmable Voice with your Django application.

Pre-requisites

  • Twilio account. If you don't have a Twilio account, you can create one here .
  • Twilio number. Follow the prompts when signing up, and you'll be redirected to a page where you can get a phone number.
  • Python3. If you do not have it installed, you can install it here .
  • ngrok. This tool enables your locally running web applications to be accessible online. Install it here .
  • HTTP/API client of choice. You can install Postman here or Insomnia here . This article uses Insomnia.

You can find the complete Django project code in this GitHub repository.

After completing the project in this article, you should expect an experience similar to the one illustrated in this video .

Set up the Django app and database

Create a new directory where the project will live. Type the following commands in your terminal:

mkdir pizzavoice
cd pizzavoice

To isolate your project dependencies, you'll start by creating a virtual environment. Use the following command:

python -m venv venv

This will allow your project to be set up with specific dependencies without affecting the system-wide Python environment or other virtual environments.

Activate your virtual environment using the following command:

source venv/bin/activate

When the environment is active, you should see its name in brackets in the prompt.

(venv) user@hostname:~/your_project$

You can type deactivate in the terminal to deactivate the virtual environment, but keep it active for now.

With your environment all set up, install the first dependency, which is, you guessed it, Django.

 

pip install django

Start a new Django project with the following command:

django-admin startproject pizzaproject

To ensure everything works well, change into the project directory and run the Django development server:

cd pizzaproject
python manage.py runserver

Copy the URL in the output and paste it into your web browser to see the following page:

Django is running successfully.

Congratulations. You have successfully created and run a Django project.

Close the server by typing CTR+C in the terminal.

Create an app inside of the pizzaproject directory.

python manage.py startapp pizza

Let's check on the structure of the pizza app you just created. Inside the pizza directory, type tree onto the terminal line.

This will show you the structure of the project.

.
├── manage.py
├── pizza
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── pizzaproject

Django creates these files by default, where:

  • models.py file contains the necessary fields and behaviors of the data you're storing.
  • admin.py file is Django's command-line utility that performs administration tasks.
  • views.py file contains Python functions that take HTTP requests and return HTTP responses, like HTML documents.

Understanding these files will make it easier for you to start and manage your Django project.

Next, you will install the other necessary apps, Django Rest Framework and Twilio Python SDK.

Integrate the Django Rest Framework in to your application

Django REST framework is a powerful and flexible toolkit for building Web APIs.

Install DRF in the command line using pip as follows:

pip install djangorestframework

Install the Twilio Python SDK

The Twilio Python SDK, for this case, will be used to create the TwiML.

Install it as follows:

 

pip install twilio

Open the project using your favorite text editor or IDE . Navigate to your project's settings.py file and add pizza and rest_framework to the INSTALLED_APPS list to integrate them into your project as seen below:

 

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pizza',  #here
    'rest_framework', #here
]

Create Django models to represent your data

Let's discuss the logic for the database. Customers will pick the size of the crust, and then the topping they desire, and that will be all for the order. So, all you need to store is the crust sizes and the toppings, both with their respective prices.

Navigate to the pizza app directory, where models.py is. Add the following to the models.py file:

from django.db import models

# Create your models here.
SIZES = [
    (1,'small'),
    (2,'medium'),
    (3,'large'),
    (4,'extra large')
]

class Crust(models.Model):
    size = models.IntegerField(choices=SIZES)
    price = models.DecimalField(max_digits=4,decimal_places=2)
 
    def __str__(self):
        return f"{self.size} @ {self.price}"
    @property
    def display_name(self):
        return SIZES[self.size -1][1]
 
    class Meta:
        unique_together = [['size','price']]
 
class Topping(models.Model):
    name = models.CharField(max_length=100,unique=True)
    price = models.DecimalField(max_digits=5,decimal_places=2)
 
    def __str__(self):
        return self.name
class Order(models.Model):
    phone_number = models.CharField(max_length=20)
    crust = models.ForeignKey(Crust,on_delete=models.CASCADE)
    topping = models.ForeignKey(Topping,on_delete=models.CASCADE,null=True)
    draft = models.BooleanField(default=True)
 
class OrderMessageConfig(models.Model):
    welcome_message = models.TextField(max_length=100)
 
    def save(self,*args,**kwargs):
        self.__class__.objects.exclude(id=self.id).delete()
        super(OrderMessageConfig, self).save(*args, **kwargs)

The Crust model manages crust sizes and their corresponding prices, enforcing unique size and price combinations. The sizes have been hard-coded into the system and can be accessed as choices in the Crust model. The Topping model will store the various pizza toppings with individual names and their prices.

You will also need a way to track your orders. The Order model stores customer orders, including phone numbers, chosen crust and toppings, and a draft flag to determine whether the order is complete.

The OrderMessageConfig model stores configuration data for order-related messages; the welcome_message will be what the customer first hears when they call the Twilio number.

These models are the core building blocks for creating your application.

Register the models in the admin panel

The admin.py file inside your app folder is where you register your models to make them accessible and manageable through the Django admin interface. Registering models allows administrators to interact with and manage the data stored in those models through a user-friendly web interface.

Add the following code to the admin.py file:

from django.contrib import admin
from pizza.models import Crust,Topping,Order,OrderMessageConfig
# Register your models here.
admin.site.register(Crust)
admin.site.register(Topping)
admin.site.register(Order)
admin.site.register(OrderMessageConfig)

 

The admin.site.register() function associates each model with the admin panel. After registration, you can perform various administrative tasks, such as adding, editing, and viewing data related to these models.

Migrate the models in Django to apply the changes to your database

It is essential to migrate the models before you can start using the admin panel to manage them.

Migrations in Django serve as the mechanism for applying model changes to your database. After you run the migrations, your database will know the changes you made in the models.py file.

Change the directory to the root of your project where manage.py is located. Write these commands in the terminal:

python manage.py makemigrations
python manage.py migrate

The command makemigrationscreates migration files that will then be used by the migrate command to create the schema defined in the database.

Create a superuser for your SQL database

Before you can access the admin panel, you will need to create a super user. A super user has complete control over the administrative aspects of a database.

Execute the following command in the terminal:

./manage.py createsuperuser

Follow the prompts to input the username and password.

After successfully creating your user, run python manage.py runserver again to restart your server.

Open your web browser and navigate to http://127.0.0.1:8000/admin . Notice the /admin in the URL; this is the route to your admin panel. Enter the username and password you created in the previous step. You should now have access to your admin panel.

Django admin interface

Add data through the Django admin panel. It is a convenient way to populate your application with initial test data.

Assign a price to each crust size, from small to extra large. Add the types of toppings available for pizza, for example, pepperoni and mushrooms, each with pricing. Finally, add a welcome message that one would hear when they call.

Understand the architecture of your app

So far, so good. With the test data, you can start creating the API that will interface with Twilio Programmable Voice to give you the IVR.

Take a moment to understand the overall architecture of the app.

overall architecture of the app

Twilio will communicate with your application via HTTP REST APIs . You could use Django itself to create a REST API, but that would be cumbersome and involve much re-inventing of the wheel. Instead, you will use a third-party Django package called Django Rest Framework (DRF). It is a powerful, robust, and battle-tested package that will make your API creation much easier, providing serialization, authentication, and other valuable features.

DRF will be the link between Twilio Programmable Voice and your Pizza app. When a customer initiates a call to the registered phone number, Twilio sends an HTTP request to your app using the endpoints you will expose through DRF. Your app subsequently processes this request, using Twilio's Python SDK to generate TwiML (XML that Twilio Programmable Voice understands). Finally, your app sends this XML data back to Twilio. Twilio will then use the data to generate the corresponding voice commands for your caller.

Integrate the app with Twilio

This section will take you through the process of integrating your application with Twilio.

Create the REST API

Using your IDE or text editor, navigate to your pizza app directory, and create a new folder called api.

You will start by setting up an entry point to the app. Whenever a customer calls your number, Twilio will automatically access this code through a webhook .

Create two new files inside the folder named views.py and urls.py.

Open views.py and add the following code:

from django.urls import reverse
from rest_framework.views import APIView
from django.http import HttpResponse
 
from twilio.twiml.voice_response import VoiceResponse
from pizza.models import OrderMessageConfig,Crust,Order,Topping
 
class Welcome(APIView):
    def post(self,request):
        """
            Respond to incoming calls, give the users options.
        """
        #construct the message
        message = OrderMessageConfig.objects.first().welcome_message
        #add the crust options
        for i,crust in enumerate( Crust.objects.all() ):
            message +=  f" press {crust.size} for {crust.display_name},"
        response = VoiceResponse()
        with response.gather(
            num_digits=1, action=reverse('topping'), method="POST"
        ) as g:
            g.say(message=message, loop=3)
        return HttpResponse(str(response),status=200,content_type='text/xml')

The above code first imports the necessary modules and packages. It includes:

  • reverse from Django's URLs module for dynamic URL generation.
  • VoiceResponse class from the Twilio Python SDK allows for the creation of TwiML responses.

When a call is received, this view constructs a welcome message by fetching data from the OrderMessageConfig model. It then dynamically adds the available pizza crust sizes retrieved from the Crust model to the message.

Each crust size is associated with a digit, which users press/tap on their phone keypad. This is where Twilio's VoiceResponse comes into play. The view uses it to create an XML response with a gather element, instructing Twilio to listen for user input.

Upon receiving a digit from the caller, it instructs Twilio to navigate to the topping URL, passing along the selected digit for further processing.

 

Next is the Topping view. Whatever digit was chosen in the previous interaction will be passed here as Digits in the POST data of the request, and this information will be used to determine the selected crust size. Create the view in views.py with the following code:

 

class Toppings(APIView):
    def post(self,request):
        """
            Handle menu items
        """
        crust_option = request.POST.get('Digits')
        phone_number = request.POST.get('Caller')
        #create the order
        order = Order.objects.create(
            crust=Crust.objects.get(size=int(crust_option)),
            phone_number = phone_number            
        )
        #create message
        message = f"You have selected {order.crust.display_name} size,"
        for i,topping in enumerate( Topping.objects.all() ):
            message += f"press {i+1} for {topping.name},"
            pass
        response = VoiceResponse()
        with response.gather(
            num_digits=1,
            action=reverse('finalize',kwargs={"order_id":order.id}),
            method="POST"
        ) as g:
            g.say(message=message, loop=3)
 
        return HttpResponse(str(response),status=200,content_type='text/xml')

The Toppings view builds upon the user's input from the Welcome view and manages the selection of pizza toppings. The view creates a new order entry in the Order model, associating it with the chosen crust size and the caller's phone number for contact purposes.

The view then constructs a message listing all available toppings by iterating through the Topping model, giving each topping a corresponding digit for user selection. Like the Welcome view, it utilizes Twilio's VoiceResponse to create an XML response with a gather element. This element instructs Twilio to listen for further user input. Once a digit is received, the instruction redirects to the finalize URL, passing on the selected digit and the order's ID for subsequent processing.

Finally, create the FinalizeOrder view in views.py using the following code:

class FinalizeOrder(APIView):
 
    def post(self,request,order_id):
        topping_option = request.POST.get('Digits')
        topping = Topping.objects.all()[int(topping_option)-1]
        #the order
        order = Order.objects.get(id=order_id)
        order.topping = topping
        order.draft = False
        order.save()
        #pricing
        total = order.crust.price + order.topping.price
        message = f"Your order for {order.crust.display_name} {order.topping.name} Pizza has been made successfully,"
        message += f"Order ID is {order.id},"
        message += f"Total price is {total},Thank you for choosing our Pizza"
        # Start our TwiML response
        resp = VoiceResponse()
        # Read a message aloud to the caller
        resp.say(message, voice='Polly.Amy')
        return HttpResponse(str(resp),status=200,content_type='text/xml')

The FinalizeOrder view completes the pizza order process. It takes the user's final input, representing a topping choice, and fetches the corresponding topping from the Topping model. It identifies the order to update based on the provided order ID and associates it with the selected topping. The topping is the last step to ensure the order is confirmed.

Additionally, it calculates the total order price based on the selected crust and topping prices. It then constructs a confirmation message, which includes details such as the crust size, topping name, order ID, and the total cost. The view utilizes Twilio's VoiceResponse to create an XML response to deliver this message to the caller. The message is spoken to the caller, providing confirmation and gratitude for choosing the pizza service.

You will need to create URLS corresponding to the views you just created. These are the endpoints that can be accessed via http.

Open the urls.py file in the api folder and add the following:

 

from django.urls import path
from pizza.api import views
 
urlpatterns = [
    path('welcome/',views.Welcome.as_view(),name="welcome"),
    path('topping/',views.Toppings.as_view(),name="topping"),
    path('finalize-order/<int:order_id>/',views.FinalizeOrder.as_view(),name="finalize")
]

The code above defines the URL routing configuration for the app's api views.

  • The /welcome/ URL pattern is associated with the Welcome view, handles incoming calls, and presents users with pizza crust options.
  • The /topping/ pattern corresponds to the Toppings view, managing topping selections.
  • The /finalize-order/int:order_id/ URL pattern maps to the FinalizeOrder view, which is responsible for processing and confirming pizza orders. It expects an order_id parameter, allowing the view to identify and handle specific orders.

Add the app's URLs to the project's URL. Navigate to the urls.py in the project folder and add the following code:

 

from django.contrib import admin
from django.urls import path,include #here
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path("orders/", include("pizza.api.urls")), #here
]

Test the REST API endpoints

You will test the endpoints to check that they return the expected XMLs.

First, include 127.0.0.1 in the ALLOWED_HOSTS setting in settings.py. This tells Django to trust requests coming in using that hostname.

ALLOWED_HOSTS = ['127.0.0.1' ]

Make sure you are in the project directory where the manage.pyfile located is to run the server:

python manage.py runserver

You will test your endpoints inside your preferred API client. I used Insomnia for this tutorial.

Use the POST method for all your HTTP requests because Twilio Programmable Voice will, by default, send POST requests to your endpoints.

First, enter the URL for the Welcome view.

 

http://127.0.0.1:8000/orders/welcome/

Click on your API client's Send or Execute button. Your response should look similar to the one shown below:

API testing of welcome view

 

Next, check the "Toppings" and create a new HTTP request in your client.

Ensure that you set each request method to POST. Set the URL field of your request to the following URL:

http://127.0.0.1:8000/orders/topping/

For this test scenario, it's essential to include Multipart/form-data content type with two key-value pairs, as this is the content type Twilio will use when you go live:

 

- Digits: The digit corresponding to the crust size the user selects during the Welcome view interaction.

- Caller: The caller's phone number, ensuring that the Toppings view associates the order with the caller.

Click the Send or Execute button.

The response should look something similar to this:

 

API testing of the toppings

Take the same steps when testing finalize_toppings. Set the URL field of your request to the following URL:

 

http://127.0.0.1:8000/orders/finalize-order/2/

This URL points to the FinalizeToppings view and includes an order_id parameter, identifying the specific order. Include multipart/form-data content type with a single key-value pair:

  • Digits: The digit corresponding to the topping choice you want to simulate.

 

Click the Send or Execute button.

Your response should look something similar to this:

 

API testing of finalize View

Expose your local server to the internet

Your endpoints are now generating the TwiML as expected, but all that is running on your local machine. How do you get Twilio to communicate with your app?

You must expose your local development server to the internet, allowing Twilio to communicate with your Django application. This is wherengrok comes in. ngrok is a tool that creates a secure tunnel from the public internet to your local machine.

 

Open a new terminal window so that you don't interfere with where your server is running.

Run ngrok as follows:

ngrok http 8000

This will create a public URL that, when accessed, ngrok will forward the requests to your local server.

Look for the "Forwarding" section in the ngrok interface. The URL is usually in the format http://random.ngrok-free.app. Copy the URL.

Copy the url  under the Forwarding section in the ngrok  interface.

Paste the ngrok URL to the ALLOWED_HOSTS list in your Django project's settings. This will tell Django to trust requests originating from the ngrok URL.

ALLOWED_HOSTS = ['random.ngrok-free.app', '127.0.0.1' ]

When you visit that URL in your browser, You should see the same interface you see with 127.0.0.1:8000.

Now that your app can be accessed via the public internet, it is time to tell Twilio where to find it.

Log in to your Twilio account and navigate to the Active Numbers tab. This is where you manage your Twilio phone numbers.

select active numbers tab

Select the phone number you want to use for testing. You'll find a section in the phone number settings where you can configure webhook URLs for various events, including incoming voice calls.

In the webhook settings for incoming voice calls, paste the ngrok URL you copied earlier. Include the specific path for the orders app by appending /orders/welcome/. This is where Twilio will point whenever a phone call is made to your number.

paste ngrok url in webhook settings

Click the Save button to save and apply your changes.

Call the number given to you by Twilio. You should hear the messages you generated in the XML and input the digits as requested.

The ngrok setup forwards Twilio's incoming POST requests to the pizza app, which processes the data related to incoming calls at the specified endpoint. The POST 200 OK response confirms the successful communication between Twilio, ngrok, and the app while handling incoming voice calls.

And that is it; you have a working Pizza Order-By-Phonecall app.

What's next for Django apps and Programmable Voice?

Congratulations on integrating Twilio Programmable Voice with your Django application.You can build your skills further by exploring these resources:

Happy coding!

Chepkirui is a Technical Writer and Back-end Developer. She enjoys learning new technologies and writing articles about them.When not coding, she is into photography. Connect with her on LinkedIn and GitHub .