Building SHAKEN/STIR verification Into Your Voice CAPTCHA IVR

September 24, 2020
Written by
Alan Klein
Reviewed by
Diane Phan
Tim Beyers
Paul Kamp


There is a lot of progress being made in the battle against spam and robocallers. But – one of the techniques spammers still abuse is CallerID, known as  CallerID spoofing. CallerID spoofing falsifies where the call originates on the public telephone network allowing the fraudster to impersonate identities – and vulnerable victims like you and me answer the phone.

Currently in beta, Twilio announced Programmable Voice and Elastic SIP Trunking now perform SHAKEN/STIR verification on all  incoming calls in the United States to your Twilio local phone numbers when a SHAKEN/STIR identity header is present. The percentage of calls with the identity header will continue to rise over the coming year. You can read more about the technical implementation details under How to make trusted calls and protect against unlawful spoofing using SHAKEN/STIR

Don’t miss Twilion Tim Beyers’s upcoming talk at SIGNAL 2020 on SHAKEN/STIR.

There’s still time to sign up – find more information here.

Due to the rise of robocalling, some customers have been forced to place a Voice CAPTCHA on their Phone numbers to protect employees from being spammed.

In this post, I’ll show you how to use serverless Twilio Studio and Functions to create a supercharged Programmable Voice CAPTCHA IVR. Callers with SHAKEN/STIR fully attested CallerID gets the VIP treatment, bypassing the CAPTCHA.


In order to follow this tutorial, you will need:

  • A Twilio account and active number. If you don’t have an account, go ahead and create one now.

App Overview and Set Up

Write the Twilio Function

Our Twilio Function’s purpose is to return random digits of a length you define in a visual Studio Flow representing your IVR. The data is returned as a JSON object with two keys, digits and spoken.

The Studio flow we build after will have a couple widgets dedicated to handling the function. The Studio Split Based On Widget evaluates digits while the Say/Play Widget uses Text to Speech (TTS) to speak out spoken, which is a comma separated string of digits that are read out as single digits rather than a large number.

If you haven’t visited Twilio Functions in a while, things may look a little different. Twilio recently released a new Functions and Assets UI! It is a fantastic upgrade and you can read more about it on the Twilio Docs here. We now have quite a number of practical Twilio Function examples on this same page, which you can modify to meet your needs for other projects.

  1. Open up your Twilio console and head to the Functions overview section.
  2. Under Overview, click the blue Create Service button.
  3. Give it a service name such as "IVRCaptcha". Click the Next button.
  4. At the top left of the screen, click the blue Add + button and select Add Function. You can leave the Function at the default, “Protected”. Protected means that only requests from your account with the proper X-Twilio-Signature can successfully execute the Function. In our case, only Twilio Studio will access our Function directly. You can read more about validating requests from Twilio on the Docs.
  5. Rename the path for your Function. I used /IVRCaptcha as seen in the screenshot below:

IVR CAPTCH Twilio Function example

Remove the existing boiler plate code to copy and paste the code below. Make sure to click save and the blue Deploy All button at the bottom left.

Verify in the Function logs that the deployment was successful. This Function is well documented, so you can dive into the details and understand what's happening in the code.

// Description
// This Twilio Function returns a random length digit sequence for use as an IVR challenge.
// The sequence length is defined in the Studio Set Variables, flowEnvironmentVariables, Widget.

// Params:
//   event.length -- number of digits to return
// Returned result object:
//   digits -- string of digits
//   spoken -- string of digits with commas and spaces, suitable for text-to-speech

exports.handler = function(context, event, callback) {
    let digits = '';
    for (i = 0; i < event.length; i++) {
        digits += Math.floor(Math.random() * 10); 
        console.log(`Output: ${digits}`);
        console.log(`Voice Output: ${digits.split('').join(', ')}`);
        result = {
            "digits": digits,
            "spoken": digits.split('').join(', ')
        return callback(null, result);

How this Function works

This Twilio Function's one purpose is to generate a random string of digits to challenge all non-verified SHAKEN/STIR callerIDs. The length is defined in the Set Variables Widget within the Studio flow that we will create next.

Studio Flow

Instead of walking through the creation of the Studio Flow widget by widget, we will import the JSON representation of the flow which you can find here. I’ll explain the flow in turn.

Import the Studio Flow

  1. To get started, go to the Manage Flows Studio page, then click either the red Create new Flow button if this is your first Flow, or the red plus (+) sign if it’s not your first Flow. Here are some screenshots of what your Studio Flow dashboard may look like:

Create a new Flow in Twilio Studio when you haven&#x27;t before


Create a new Flow in Twilio Studio when you have older flows
  1. Give your Flow a name. I called mine "SHAKENSTIRIVR-Captcha". Scroll down and select Import from JSON from the menu and click on the Next button.
  2. Replace the contents of the edit box with the JSON that can be downloaded here, and click Next.

    You should see the Studio canvas rendered out for you, as shown below:

SHAKEN/STIR IVR built in Twilio Studio
  1. You may want to edit the Set Variables Widget named flowEnvironmentVariables to set the captchaDigitLength. For our example, we will leave it at a 2 digit challenge but a 1 digit challenge may suffice for your needs. Click Save.
  2. Edit the get_random Run Function Widget and update the Function it points to by selecting the Functions Service called "IVRCaptcha" in our case. Then the Environment which defaults to "ui" for all Functions created via the UI, and finally the Function, "/IVRCaptcha". Click Save.

You will see an existing Function Parameter which obtains its value from the earlier Set Variables Widget and tells our Twilio Function how long of a random digit string to create and return back to Studio. Studio is able to incorporate this information back into our flow by intelligently parsing the returned JSON. You can find out more about how the Run Function works in the Twilio Docs.

  1. Edit the captcha Gather Input on Call Widget and adjust the Stop Gathering After field to match the number of digits you entered for the digit challenge in step # 4 above. Click Save.
  2. Don’t forget to Publish your flow!

What is this Studio Flow doing?

The Studio flow first sets the length of our CAPTCHA challenge and welcomes the caller to the *organization via the welcome Say/Play Widget. When we configure our Twilio number to point to our Studio flow, we will pass in two URL query parameters. One is called *Org which is the organization name read out in the welcome Say/Play Widget. The other is the number we will forward calls that successfully traverse through the flow, called Fwd.

Next, the check_stir Split Based On Widget checks a key sent in as part of the call metadata called This key is now exposed as part of the new SHAKEN/STIR capability we announced earlier this week. There is a lot of metadata sent into Studio which is viewable by clicking on the Studio Logs on the sidebar to the left. 

Currently, we do not have any execution logs but we will once we place a successful inbound call to our flow. When we have logs, clicking a Studio execution SID in the logs, then clicking the Trigger widget and expanding Widget & Flow Properties will show this metadata.

We are looking for a value of, TN-Validation-Passed-A, the highest level of SHAKEN/STIR attestation. If it is present, we confirm that their CallerID is verified in the status_confirmed Say/Play Widget, and bypass the CAPTCHA. We connect the caller to the callee, using the Fwd value we passed in as part of the URL query parameters defined when provisioning the number.

If the value, TN-Validation-Passed-A is not present, we proceed to the get_random, Run Function Widget, to generate a random digit CAPTCHA challenge which is read out by the captcha Gather Input On Call Widget. This same widget collects the response to the challenge, feeding it into the check_captcha Split Based On Widget. If the two values match, the calling party is greeted with a successful response in the thanks_human Say/Play Widget and connected to the callee using the connect_call Connect Call To Widget.

All failure paths are set-up to end the call through their respective widgets.

Configure your Twilio number & test it out!

Now that your Studio flow is built, let’s configure your Twilio number to test it out.

Visit the Phone Numbers console first, and select your Twilio number. You can learn how to search and buy a Twilio Phone Number if you don't have one already.

Normally, when associating a voice Studio flow with a Twilio number, you select Studio Flow under When a Call Comes In. However, in our case, since we are passing in two URL query parameters to Studio, we need to select Webhook and construct the webhook manually.

Construct your Webhook Manually

Back on the Studio canvas, click the Studio Trigger widget (the very first widget) and copy the Webhook URL to a plain text editor. It will look similar to the example below, with your unique account number for Studio flow SID:

Now, we will add the two URL query parameters, Org and Fwd, which our Studio flow consumes to the very end of this phone number Webhook URL, as shown below:

The URL with the URL query parameter appended will be pasted into the A Call Comes In and Primary Handler Fails fields for your Twilio number. The next field, Call Status Changes will take the URL without the URL query parameters appended. You can view the image below.

Add query parameters to Webhook URLs in Twilio Studio

For this example URL, note the portion beginning with the ?. The %20 value is used to handle the space in the organization name (in my case Winston Enterprises) and the %2b is converted to a "+", to represent the telephone number in E.164 format. These are called URLEncoded values, basically representations of the actual values.

(The last two fields are required to avoid stuck executions.)

Set the HTTP request to POST and click Save. You are all set to test – simply place a call to your Twilio number!

The two URL Query Parameters will disappear on this page once you click save (and be replaced with your Studio Flow name, SHAKENSTIRIVR-Captcha) but they are still there. You can view them being sent into your Studio Flow as metadata, using the Studio execution logs we mentioned earlier or by viewing your numbers via the Twilio console Active Numbers section, under the Configuration column.

Update a Twilio number to call a Studio Flow

After sending your first call to the Twilio number to test this out, you will see your first log pop up in the Studio Logs as shown in the screenshot below:

Studio log screenshot

Click on the execution SID, which always begins with "FN". Then expand the Trigger Widget and click on Widget & Flow Properties to see the details below:

SHAKEN/STIR Flow log showing a forwarding number and organization


You can view the SHAKEN/STIR status of your incoming calls in your call log. Inbound calls must have the “A” entry in the STIR STATUS column to follow through to the status_confirmed Widget in your Studio flow. STIR/SHAKEN is currently being deployed throughout the US carrier ecosystem so only a subset of inbound calls will be signed.

Industry consensus is to never block calls based on the SHAKEN/STIR attestation alone. It will be many years before all phones in the US support SHAKEN/STIR.

STIR status of incoming calls to a Studio IVR

Building SHAKEN/STIR verification Into Your Voice CAPTCHA IVR

Now you have a fully functional IVR CAPTCHA or a virtual bouncer leveraging SHAKEN/STIR signing – when available – as a VIP card. If visitors don’t present a VIP card, you will fall back and CAPTCHA them before letting them in.

There are many ways you can enhance this IVR CAPTCHA and I’ve shared a number of useful additional resources below to get you started.

You can also combine this approach with other methods to further vet incoming calls, such as using a blocklist, or an allow list with people who have called before.

Some other ways I can think of to improve your IVR’s signal to noise ratio include:

  • adding a blocklist, to deny specific problem numbers before they even enter the CAPTCHA
  • adding a persistent allow list (fast pass), for callers who have successfully passed through the CAPTCHA on a previous call.

There are many different options – I bet you will come up with the ones that best fit your unique use case, that’s what makes Twilio so powerful!

Additional Resources to enhance the IVR CAPTCHA

Alan Klein is a Principal Solutions Engineer based in Atlanta, GA Office. Alan is a Serverless solution champion and Programmable Voice and Elastic SIP Trunking SIP Expert at Twilio. He's currently focused on furthering his knowledge in Node.js and front-end development and sharing his knowledge with others. You can reach him at aklein [at] or on Twitter at @SystemsEng.

Robert Welbourn is a Principal Solutions Engineer based in the Boston, MA area, specializing in Twilio's voice products.  He can be reached at rwelbourn [at] or @RobertWelbourn on Twitter.