Conversational AI • Last Updated 5/7/2024

Building an IVR Demo with the Telnyx API and Python SDK

In under 60 mins, this demo will set you up to streamline user experiences using an Interactive Voice Response system.

By Pete Christianson

Follow this step-by-step guide to building a find me, follow me IVR using the Telnyx voice API and Python SDK.

IVR, or Interactive Voice Response technology is the cornerstone of modern contact centers and unified communications platforms.

IVR systems enable businesses to automate phone interactions, use smaller customer service teams, and ultimately reduce costs. An IVR can also streamline and enhance user experiences, with popular features like:

  • Self-serve capabilities: IVRs can handle simple customer service functions like checking balances and taking payments with greater efficiency.
  • Intelligent call routing: your IVR should be able to analyze customer inputs to route them to the most suitable and first available agent.
  • Secondary language support: offering multilingual IVR support can help improve the accessibility of your business for a greater number of customers.
  • Natural language processing: by incorporating NLP technology, your IVR can converse with customers in a more authentic, conversational way.

A find me - follow me IVR refers to two technologies that, when used together, enable phone calls to be received at different locations, on different phones - whether ringing all at once, or in sequence.

In this tutorial, you’re going to build your own find me - follow me IVR demo using the Telnyx Call Control API and Python SDK, with flask and ngrok.

Before you start building your IVR

In order to complete this IVR demo, you'll need set up a Mission Control Portal account, buy a number and connect that number to a Call Control Application. You can learn how to do that in the quickstart guide.

You’ll also need to have python installed to continue. You can check this by running the following:

$ python3 -v

Now in order to receive the necessary webhooks for our IVR demo, we'll need to set up a server. For this tutorial, we'll use Flask, a micro web server framework. A quickstart guide to flask can be found on their official website. For now, we'll install flask using pip.

$ pip install flask

Telnyx Call Control API basics

For the Voice API application you’ll need to get a set of basic functions to perform Telnyx Call Control Commands. The below list of commands are just a few of the available commands available with the Telnyx Python SDK. We'll be using a combination of Answer, Speak, and Gather Using Audio to create a base to support user interaction over the phone.

For each Telnyx Call Control Command we'll be using the Telnyx Python SDK. To execute this API we are using Python telnyx, so make sure you have it installed. If not you can install it with the following command:

$ pip install telnyx

After that you’ll be able to use ‘telnyx’ as part of your app code as follows:

import telnyx

We'll also import Flask in our application as follows:

from flask import Flask, request, Response

And set our API key using the Python telnyx SDK:

telnyx.api_key = "YOUR_TELNYX_API_KEY"

Server and webhook setup

Flask is a great application for setting up local servers. However, in order to make our code public to be able to receive webhooks from Telnyx, we'll use a tool called ngrok.
See ngrok Installation instructions . To begin our flask application, underneath the import and setup lines detailed above, we'll add the following:

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def respond():
    //Our code for handling the call control application will go here
    print(request.json[‘data’])
return Response(status=200)

This is the base Flask application code specified by their documentation. This is the minimum setup required to receive webhooks and manipulate the information received in json format. To complete our setup, we must run the following to set up the Flask environment (note: YOUR_FILE_NAME will be whatever your .py file is named):

$ export FLASK_APP=YOUR_FILE_NAME.py

Now, we're ready to serve up our application to our local server. To do this, run:

$ flash run

A successful output log should look something like:

 * Serving Flask app "main"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Now that our Flask application is running on our local server, we can use ngrok to make this public to receive webhooks from Telnyx by running the following command wherever the ngrok executable is located (note: you may have to open another terminal window or push the Flask process to the background):

$ ./ngrok http 5000

Once this is up and running, you should see the output URL in the command logs or located on the ngrok dashboard page. This URL is important because it will be where our Call Control Application will be sending webhooks to. Grab the URL and head on over to the Telnyx Dashboard page.

Navigate to your Call Control Application and add the URL to the section labeled "Send a webhook to the URL" as shown below. Add the ngrok URL to that section and we're all set up to start building our IVR demo!

URL Webhook Section

Receiving and interpreting webhooks

We'll be configuring our respond function to handle certain incoming webhooks and execute call control commands based on what the values are. Flask catches the incoming webhooks and calls the respond() function every time a webhook is sent to the route we specified as ‘/webhook’. We can see the json value of the hook in the request.json object. Here's what a basic Telnyx Call Object looks like:

{
    'data': {
        'event_type': 'call.initiated',
        'id': 'a2fa3fa6-4e8c-492d-a7a6-1573b62d0c56',
        'occurred_at': '2020-07-10T05:08:59.668179Z',
        'payload': {
            'call_control_id': 'v2:rcSQADuW8cD1Ud1O0YVbFROiQ0_whGi3aHtpnbi_d34Hh6ELKvLZ3Q',
            'call_leg_id': '76b31010-c26b-11ea-8dd4-02420a0f6468',
            'call_session_id': '76b31ed4-c26b-11ea-a811-02420a0f6468',
            'caller_id_name': '+17578390228',
            'client_state': None,
            'connection_id': '1385617721416222081',
            'direction': 'incoming',
            'from': '+14234567891',
            'start_time': '2020-07-10T05:08:59.668179Z',
            'state': 'parked',
            'to': '+12624755500'
        },
        'record_type': 'event'
    },
    'meta': {
        'attempt': 1,
        'delivered_to': 'http://59d6dec27771.ngrok.io/webhook'
    }
}

We want to first check and see if the incoming webhook is an event. To check that, we need to look at the record_type using the following check:

def respond():
    //Check record_type of object
    data = request.json['data']
        if data.get('record_type') == 'event':

    print(request.json[‘data’])
return Response(status=200)

Then, we can check and see what kind of event it is. In the case of the example json above, the event is call.initiated. We can get that value using the following added code:

def respond():
    //Check record_type of object
    data = request.json['data']
        if data.get('record_type') == 'event':
        //Check event type
        event = data.get('event_type')
            print(event, flush=True)
            if event == "call_initiated":
                print("Incoming call", flush=True)

    print(request.json[‘data’])
return Response(status=200)

As you can see, this check will print out “incoming call” whenever a call.initiated event is received by our application. We can even test it by giving the Phone Number associated with our Call Control Application a call! Now we can start to implement some commands in response to this webhook, in order to build our IVR demo.

Call commands

See full reference to the call commands in every Telnyx SDK available.

Client state

Within some of the Telnyx Call Control Commands list we presented, you probably noticed we were including the Client State parameter. Client State is the key to ensure that we can perform functions only when very specific conditions are met on our App while consuming the same Call Control Events.

Because Call Control is stateless and async your application will be receiving several events of the same type, e.g. user just included DTMF. With Client State you enforce a unique ID to be sent back to Telnyx which be used within a particular Command flow and identifying it as being at a specific place in the call flow.

This app in particular will bridge two seperate calls together in the event the user chooses to accept the call. Thus the call_control_id of the pending bridge call must be mapped, and not be risked to being stored in a variable which could be re-assigned while we are waiting for gather response - should a new call be intiated.

Building your IVR demo

With all the basic Telnyx Call Control Commands set, we're now ready to consume them and put them in the order that will create the IVR demo. For this tutorial we want to keep it simple with a flow that corresponds to the following IVR logic:

  • Allow the incoming call to be parked.
  • Execute dial function to the user's PSTN number.
  • Present an IVR allowing them to Accept or Reject the call and execute a 20 second timeout to hangup for no answer.
  • When the user answers, they will be met with an IVR Greeting:
    • Press 1 to Accept the Call - The Parked Call and this Dialed call will now be Bridged. The Timeout to Hangup the Dial call to user will be cleared.
    • Press 2 to Reject the call - The Dialed Call will hang up. The Parked call will enter the Voicemail Functionality via Speak and Recording Start.
    • At any time during the caller, the user can press *9 to initiate on demand call recording.
  • An SMS notification will be sent to the user to notify them of a call recording or voicemail message.

IVR Demo Diagram

Creating an object to store call information

Above our respond function required by Flask, we can create a simple class to hold our call information. This will hold our call_control_id and client_state variables.

class my_IVR_info:
    call_control_id: ''
    client_state: ''

my_ivr = my_IVR_info()

This global variable will be referenced in our respond function, so make sure to declare it as a global inside it. Our file should now look like this.

import telnyx
from flask import Flask, request, Response
telnyx.api_key = "YOUR_API_KEY"

app = Flask(__name__)

class my_IVR_info:
    call_control_id: ''
    client_state: ''

my_ivr = my_IVR_info()

@app.route('/webhook', methods=['POST'])
def respond():
    global my_ivr
    data = request.json

    if data.get('record_type') == 'event':

        event = data.get('event_type')
        if event == "call.initiated":
            print("Incoming call", flush=True)

    return Response(status=200)

Answering the incoming call

Now, we can add a simple Call Command to answer the incoming call. Underneath where we check if the event is 'call_initiated', add the following:

my_ivr.call_control_id = data.get('payload').get('call_control_id')
print(telnyx.Call.answer(my_ivr), flush=True)

This code snippet simply adds the call_control_id of the incoming call to our ivr_object so that it can be passed into the Call.answer command. This code answers the incoming call, but does not yet do anything. Now we can add some call logic for our IVR demo.

Presenting IVR user options

Now that we've answered the call, we can use the Gather Using Speak command to present some options to the user. To do this, we'll add the following check:

 elif event == "call.answered":
     print("Call answered", flush=True)
     print(telnyx.Call.gather_using_speak(
     my_ivr, 
     payload="Call Forwarded press 1 to accept or 2 to reject", 
     language = "en-US", 
     valid_digits="123",
     voice = "female"), 
     flush=True)

Using the my_ivr object we created earlier, we can send Gather Using Speak audio to the number. This code will say 'Call Forwarded press 1 to accept or 2 to reject' to the user. Now they can press a digit and a dtmf webhook will be sent to our IVR application.

Interpreting DTMF for your IVR

Our next check will be to see what digit is pressed when a dtmf webhook is received. The code for this implementation is as follows:

elif event == "dtmf":
    digit = data.get('payload').get('digit')
    print(digit)

This simple code collects the digit that is pressed and stores it in a variable that is then printed to the console. In this way, your IVR determine what digit was pressed by the user and act accordingly. Another way we can collect information about what is pressed is by waiting for the gather_ended webhook. This webhook contains a digits field in the payload that contains the pressed digits.

Acting on digit responses

Now that we have our call system set up to receive DTMF and collect which digits are pressed, we're ready to execute other call commands based on that information, as shown below.

elif event == "dtmf":
    digit = data.get('payload').get('digit')
    if int(digit) == 1:
      //Do something
    elif int(digit) == 2:
      //Do something else

Congrats! You've just built a working IVR.

We now have a working baseline to support user interaction and flow for an IVR. Experiment with the different Call Control Commands and tailor this application however you like. Take a look at the Github Repo for a commented version of this code to use as a base for your IVR application!

Questions? Join our dedicated developer slack community to get the answers you need.

Share on Social

Related articles

Sign up and start building.