Call Forwarding Application Migration from TwiML to FlexML

If you have read the Migrating from Twilio to CarrierX Quick Start, you could see that TwiML and FlexML are not too much different. Both offer a special syntax to provide instructions, and all you need is to change the representation of these instructions in your code.

But when it comes to a real-case migration from Twilio to CarrierX, users might meet some difficulties.

Let’s see such a migration in details and learn how to solve the issues that arise so that your migrated application worked with CarrierX flawlessly.

Getting Application Source Code

We take the Call Forwarding application from Twilio as an example. This sample application connects callers to the offices of their U.S. senators. You can download the application source code at GitHub.

Once you download the application, you can see that it has the following structure:

[call_forward_flask]
[migrations]
[tests]
.gitignore
.mergify.yml
.travis.yml
LICENSE
README.md
black.toml
free-zipcode-database.csv
manage.py
requirements-to-freeze.txt
requirements.txt
senators.json
setup.cfg

The call_forward_flask folder holds the files we are going to modify. Here is its structure:

[static]
[templates]
__init__.py
config.py
models.py
parsers.py
views.py

Actually, the only file we need to modify here is views.py. All the routes used to send requests and responses for our application are here.

Modifying Routes

The views.py file contains the following routes:

  1. hello is the basic route that the application uses as a placeholder to the requests sent to the website root.
  2. callcongress is the main route that the application uses for incoming calls and collecting the information about the state from which the caller originates.
  3. state_lookup is the route where the application looks up for the state from the entered ZIP code.
  4. collect_zip is the route that the application uses for collecting the ZIP information if the initial state guess is wrong.
  5. set_state is the route where the application uses to set the state for the senators selection.
  6. call_senators is the route for connecting caller to both of their senators.
  7. call_second_senator is the route that the application uses to forward the caller to their second senator.
  8. end_call is the route that the application uses to thank user and hang up.

Let’s take a look at each of the routes and see what, where, and how should be modified.

I. hello Route

The hello route does not contain any TwiML specific code, so we can leave it as is:

@app.route('/')
def hello():
    return render_template('index.html')

II. callcongress Route

We modify the callcongress route like this:

  1. The callcongress route uses the VoiceResponse() class to build the TwiML response to the call. In FlexML we do not need it, so we can remove this line.

  2. The next code portion to change is the way the application gets the data from the call. Twilio sends the call data in the form of an immutable MultiDict. In CarrierX, it is pure JSON, which you can receive and parse using the common Flask request module. Refer to the code below to see how we replace the definition of the from_state variable.

  3. The if statement in TwiML forms a response with the Gather verb. In FlexML we need to change the response to plain FlexML syntax.

  4. The else statement in TwiML also forms a response with the Gather verb but with other parameters. We also change it to FlexML syntax.

  5. Before returning the response, TwiML needs to form the final response code using the append() function. In FlexML we do not need it, so we remove this line.

  6. Finally, we return the resulting response to the call with the return statement. We do not need any additional parameters for this in FlexML.

TwiML Python Code
@app.route('/callcongress/welcome', methods=['POST'])
def callcongress():
    response = VoiceResponse()
    from_state = request.values.get('FromState', None)

    if from_state:
        gather = Gather(
            num_digits=1,
            action='/callcongress/set-state',
            method='POST',
            from_state=from_state,
        )
        gather.say(
            "Thank you for calling congress! It looks like "
            + "you\'re calling from {}. ".format(from_state)
            + "If this is correct, please press 1. Press 2 if "
            + "this is not your current state of residence."
        )

    else:
        gather = Gather(num_digits=5, action='/callcongress/state-lookup', method='POST')
        gather.say(
            "Thank you for calling Call Congress! If you wish to "
            + "call your senators, please enter your 5-digit zip code."
        )

    response.append(gather)
    return Response(str(response), 200, mimetype="application/xml")

Corresponding FlexML Python Syntax
@app.route('/callcongress/welcome', methods=['POST'])
def callcongress():
    data = request.get_json()
    from_state = data.get('FromState','')

    if from_state:
        response = f'''
            <Response>
                <Gather numDigits="1" action="/callcongress/set-state" method="POST">
                    <Say>Thank you for calling congress! It looks like you\'re calling from {from_state}. If this is correct, please press 1. Press 2 if this is not your current state of residence.</Say>
                </Gather>
            </Response>'''

    else:
        response = f'''
            <Response>
                <Gather numDigits="5" action="/callcongress/state-lookup" method="POST">
                    <Say>Thank you for calling Call Congress! If you wish to call your senators, please enter your 5-digit zip code.</Say>
                </Gather>
            </Response>'''

    return response

III. state_lookup Route

We modify the state_lookup route like this:

  1. Like in the previous callcongress route, we change the way the route receives the data from the call so that JSON data was parsed and the application could get the value from the Digits key.

  2. The return statement contains the FlexML code for the Redirect verb.

TwiML Python Code
@app.route('/callcongress/state-lookup', methods=['GET', 'POST'])
def state_lookup():
    zip_digits = request.values.get('Digits', None)

    zip_obj = Zipcode.query.filter_by(zipcode=zip_digits).first()

    return redirect(url_for('call_senators', state_id=zip_obj.state_id))

Corresponding FlexML Python Syntax
@app.route('/callcongress/state-lookup', methods=['GET', 'POST'])
def state_lookup():
    data = request.get_json()
    zip_digits = data.get('Digits','')

    zip_obj = Zipcode.query.filter_by(zipcode=zip_digits).first()

    return f'''
        <Response>
            <Redirect>{url_for('call_senators', state_id=zip_obj.state_id)}</Redirect>
        </Response>'''

IV. collect_zip Route

We modify the collect_zip route like this:

  1. Remove the use of the Twilio VoiceResponse() class.

  2. Remove the forming of the Gather response.

  3. Remove the forming of the final response which uses the append() function.

  4. Replace the return statement with the one containing the FlexML code for the Gather verb.

TwiML Python Code
@app.route('/callcongress/collect-zip', methods=['GET', 'POST'])
def collect_zip():
    response = VoiceResponse()

    gather = Gather(num_digits=5, action='/callcongress/state-lookup', method='POST')
    gather.say(
        "If you wish to call your senators, please " + "enter your 5-digit zip code."
    )

    response.append(gather)
    return Response(str(response), 200, mimetype="application/xml")

Corresponding FlexML Python Syntax
@app.route('/callcongress/collect-zip', methods=['GET', 'POST'])
def collect_zip():
    return f'''
        <Response>
            <Gather numDigits="5" action="/callcongress/state-lookup" method="POST">
                <Say>If you wish to call your senators, please enter your 5-digit zip code.</Say>
            </Gather>
        </Response>'''

V. set_state Route

We modify the set_state route like this:

  1. Change the way the route receives the data from the call so that JSON data was parsed and the application could get the value from the Digits key.

  2. The same way we change the code for receiving the CallerState value.

  3. The first return statement (inside the if statement) contains the FlexML code for the Redirect verb.

  4. And the second return statement also contains the FlexML code for another Redirect verb.

TwiML Python Code
@app.route('/callcongress/set-state', methods=['GET', 'POST'])
def set_state():
    digits_provided = request.values.get('Digits', None)

    if digits_provided == '1':
        state = request.values.get('CallerState')
        state_obj = State.query.filter_by(name=state).first()
        if state_obj:
            return redirect(url_for('call_senators', state_id=int(state_obj.id)))

    return redirect(url_for('collect_zip'))

Corresponding FlexML Python Syntax
@app.route('/callcongress/set-state', methods=['GET', 'POST'])
def set_state():
    data = request.get_json()
    digits_provided = data.get('Digits','')

    if digits_provided == '1':
        state = data.get('CallerState','')
        state_obj = State.CallerState.filter_by(name=state).first()
        if state_obj:
            return f'''
                <Response>
                    <Redirect>{url_for('call_senators', state_id=int(state_obj.id))}</Redirect>
                </Response>'''

    return f'''
        <Response>
            <Redirect>{url_for('collect_zip')}</Redirect>
        </Response>'''

VI. call_senators Route

We modify the call_senators route like this:

  1. Remove the use of the Twilio VoiceResponse() class.

  2. Remove the forming of the Say and Dial responses.

  3. Replace the return statement with the one containing the FlexML code for the Say and Dial verbs.

TwiML Python Code
@app.route('/callcongress/call-senators/<state_id>', methods=['GET', 'POST'])
def call_senators(state_id):
    senators = State.query.get(state_id).senators.all()

    response = VoiceResponse()

    first_call = senators[0]
    second_call = senators[1]

    response.say(
        "Connecting you to {}. ".format(first_call.name)
        + "After the senator's office ends the call, you will "
        + "be re-directed to {}.".format(second_call.name)
    )
    response.dial(
        first_call.phone, action=url_for('call_second_senator', senator_id=second_call.id)
    )

    return Response(str(response), 200, mimetype="application/xml")

Corresponding FlexML Python Syntax
@app.route('/callcongress/call-senators/<state_id>', methods=['GET', 'POST'])
def call_senators(state_id):
    senators = State.query.get(state_id).senators.all()

    first_call = senators[0]
    second_call = senators[1]

    return f'''
        <Response>
            <Say>Connecting you to {first_call.name}. After the senator's office ends the call, you will be re-directed to {second_call.name}.</Say>
            <Dial action="{url_for('call_second_senator', senator_id=second_call.id)}">{first_call.phone}</Dial>
        </Response>'''

VII. call_second_senator Route

We modify the call_second_senator route like this:

  1. Remove the use of the Twilio VoiceResponse() class.

  2. Remove the forming of the Say and Dial responses.

  3. Replace the return statement with the one containing the FlexML code for the Say and Dial verbs.

TwiML Python Code
@app.route('/callcongress/call-second-senator/<senator_id>', methods=['GET', 'POST'])
def call_second_senator(senator_id):
    senator = Senator.query.get(senator_id)

    response = VoiceResponse()
    response.say("Connecting you to {}.".format(senator.name))
    response.dial(senator.phone, action=url_for('end_call'))

    return Response(str(response), 200, mimetype="application/xml")

Corresponding FlexML Python Syntax
@app.route('/callcongress/call-second-senator/<senator_id>', methods=['GET', 'POST'])
def call_second_senator(senator_id):
    senator = Senator.query.get(senator_id)

    return f'''
        <Response>
            <Say>Connecting you to {senator.name}.</Say>
            <Dial action="{url_for('end_call')}">{senator.phone}</Dial>
        </Response>'''

VIII. end_call Route

Finally, we modify the end_call route like this:

  1. Remove the use of the Twilio VoiceResponse() class.

  2. Remove the forming of the Say and Hangup responses.

  3. Replace the return statement with the one containing the FlexML code for the Say and Hangup verbs.

TwiML Python Code
@app.route('/callcongress/goodbye', methods=['GET', 'POST'])
def end_call():
    response = VoiceResponse()
    response.say(
        "Thank you for using Call Congress! " + "Your voice makes a difference. Goodbye."
    )
    response.hangup()

    return Response(str(response), 200, mimetype="application/xml")

Corresponding FlexML Python Syntax
@app.route('/callcongress/goodbye', methods=['GET', 'POST'])
def end_call():
    return f'''
        <Response>
            <Say>Thank you for using Call Congress! Your voice makes a difference. Goodbye.</Say>
            <Hangup/>
        </Response>'''

Finishing Migration

Now that we modified all the routes, we can safely remove the Twilio library import declaration from the beginning of the views.py file:

from twilio.twiml.voice_response import VoiceResponse, Gather
from call_forward_flask import app
from call_forward_flask.models import (
    Senator,
    State,
    Zipcode,
)

from flask import (
    Response,
    redirect,
    render_template,
    request,
    url_for,
)

@app.route('/')
def hello():
    """Very basic route to landing page."""
    return render_template('index.html')


@app.route('/callcongress/welcome', methods=['POST'])
def callcongress():
    """Verify or collect State information."""
    data = request.get_json()

    from_state = data.get('FromState','')

    if from_state:
        response = f'''
            <Response>
                <Gather numDigits="1" action="/callcongress/set-state" method="POST">
                    <Say>Thank you for calling congress! It looks like you\'re calling from {from_state}. If this is correct, please press 1. Press 2 if this is not your current state of residence.</Say>
                </Gather>
            </Response>'''

    else:
        response = f'''
            <Response>
                <Gather numDigits="5" action="/callcongress/state-lookup" method="POST">
                    <Say>Thank you for calling Call Congress! If you wish to call your senators, please enter your 5-digit zip code.</Say>
                </Gather>
            </Response>'''

    return response

@app.route('/callcongress/state-lookup', methods=['GET', 'POST'])
def state_lookup():
    """Look up state from given zipcode.

    Once state is found, redirect to call_senators for forwarding.
    """
    data = request.get_json()
    zip_digits = data.get('Digits','')
    # NB: We don't do any error handling for a missing/erroneous zip code
    # in this sample application. You, gentle reader, should to handle that
    # edge case before deploying this code.
    zip_obj = Zipcode.query.filter_by(zipcode=zip_digits).first()

    return f'''
        <Response>
            <Redirect>{url_for('call_senators', state_id=zip_obj.state_id)}</Redirect>
        </Response>'''

@app.route('/callcongress/collect-zip', methods=['GET', 'POST'])
def collect_zip():
    """If our state guess is wrong, prompt user for zip code."""

    return f'''
        <Response>
            <Gather numDigits="5" action="/callcongress/state-lookup" method="POST">
                <Say>If you wish to call your senators, please enter your 5-digit zip code.</Say>
            </Gather>
        </Response>'''


@app.route('/callcongress/set-state', methods=['GET', 'POST'])
def set_state():
    """Set state for senator call list.

    Set user's state from confirmation or user-provided Zip.
    Redirect to call_senators route.
    """
    # Get the digit pressed by the user
    data = request.get_json()
    digits_provided = data.get('Digits','')

    # Set state if State correct, else prompt for zipcode.
    if digits_provided == '1':
        state = data.get('CallerState','')
        state_obj = State.CallerState.filter_by(name=state).first()
        if state_obj:
            return f'''
                <Response>
                    <Redirect>{url_for('call_senators', state_id=int(state_obj.id))}</Redirect>
                </Response>'''

    return f'''
        <Response>
            <Redirect>{url_for('collect_zip')}</Redirect>
        </Response>'''

@app.route('/callcongress/call-senators/<state_id>', methods=['GET', 'POST'])
def call_senators(state_id):
    """Route for connecting caller to both of their senators."""
    senators = State.query.get(state_id).senators.all()

    first_call = senators[0]
    second_call = senators[1]

    return f'''
        <Response>
            <Say>Connecting you to {first_call.name}. After the senator's office ends the call, you will be re-directed to {second_call.name}.</Say>
            <Dial action="{url_for('call_second_senator', senator_id=second_call.id)}">{first_call.phone}</Dial>
        </Response>'''

@app.route('/callcongress/call-second-senator/<senator_id>', methods=['GET', 'POST'])
def call_second_senator(senator_id):
    """Forward the caller to their second senator."""
    senator = Senator.query.get(senator_id)

    return f'''
        <Response>
            <Say>Connecting you to {senator.name}.</Say>
            <Dial action="{url_for('end_call')}">{senator.phone}</Dial>
        </Response>'''

@app.route('/callcongress/goodbye', methods=['GET', 'POST'])
def end_call():
    """Thank user & hang up."""
    return f'''
        <Response>
            <Say>Thank you for using Call Congress! Your voice makes a difference. Goodbye.</Say>
            <Hangup/>
        </Response>'''

You can also remove the importing of Twilio modules from the requirements-to-freeze.txt and requirements.txt files.

Follow the instructions from the application GitHub page to run the application. Then associate the application with the CarrierX phone number, and call that number to check how the application works.

Further Reading

You have successfully migrated the Call Forwarding application from TwiML to FlexML!

Refer to the following pages to learn more about FlexML verbs and how to use them, and about ways to set up a FlexML endpoint:

Use our Migrating from Twilio to CarrierX Quick Start to learn more about other difficulties you can meet while migrating from Twilio to CarrierX and the ways to solve these issues.

Read other instructions on real-case migrations from Twilio to CarrierX here:

Refer to our other quick start guides for instructions on how to work with CarrierX: