Fun Call Flows Using SHAKEN/STIR Caller ID Attestation Levels

Person on a call on the computer using headphones
April 26, 2023
Written by
Matt Coser
Twilion
Reviewed by
Seif Hateb
Twilion
Paul Kamp
Twilion

Sick of being SHAKEN down by robocallers? In this post, we will STIR it up by using Caller ID Attestation levels to route inbound calls with various Programmable Voice features.  

SHAKEN/STIR joke meme

What is SHAKEN/ STIR?

Let’s start from the beginning. Well, maybe not the very beginning – but let’s start with a basic understanding of the SHAKEN and STIR protocols and how they are currently implemented.

You may notice these protocols being used simultaneously as SHAKEN/STIR or STIR/SHAKEN. Either is correct, and both mean the same thing.

  • STIR - Secure Telephone Identity Revisited
    • STIR is a protocol developed by IETF, describing the process of verifying if a party is allowed to use a certain number.
  • SHAKEN - Signature-based Handling of Asserted Information Using toKENs
    • SHAKEN focuses on how STIR should be implemented and deployed to carrier networks.

Before the early 2000s, caller ID spoofing was much more difficult. As SIP started gaining popularity, spoofing increased because SIP From numbers could be very easily changed without the recipient knowing.

You may also have noticed robocalls coming from numbers with the same area code or exchange as your own number. This tactic, while less egregious than using the IRS 800 number as a From, can still deceive end users by making them think the caller is familiar or from their area.  For instance, I might think a call from the same local exchange is from my doctor, school, or someone in my community.

The Do Not Originate (DNO) List came a long way in preventing the spoofing of high-profile and recognizable numbers. SHAKEN/STIR goes further, and enables originating carriers to digitally sign the call with an Attestation Level. The Attestation Level applied to the call indicates the certainty level of the originating provider and whether the caller is actually allowed to use the Caller ID.

  • Full Attestation (A) - The identity of the caller is known, and they have the right to use the Caller ID for this call.
  • Partial Attestation (B) - The originator knows the identity of the caller, but cannot confirm their right to use the Caller ID.
  • Gateway Attestation (C) - The identity of the caller is not known, as the call originated outside of the network, or internationally. C level is applied when A or B cannot be.

Due to the flexibility of Twilio's platform, users can take advantage of SHAKEN/STIR attestation in a few different ways.

Outbound Calls

For calls originating on Twilio’s platform, a From number is required.

When you make a POST to /Calls, a new Call resource will be created.

const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = require('twilio')(accountSid, authToken);

client.calls
      .create({
         twiml: '<Response><Say>Ahoy there!</Say></Response>',
         to: '+15558675310',
         from: '+15552223214'
       })
      .then(call => console.log(call.sid));

This From number is what is displayed on the recipient’s device. By default, Twilio accounts can only make calls using a from number which is either:

  • A Twilio number owned by said account
  • A non Twilio number, validated using either
    • the Outgoing Caller ID API
    • or the Caller ID validation tool in Console

If an outbound call is made using a non validated Caller ID, or a Twilio Number not belonging to the account, the call will fail with a 21210 Error.

Error code 21210 example on a Twilio outbound call

Otherwise, Twilio will cryptographically sign the call with an attestation level that best represents the state of the Twilio number (or verified Caller ID) used to make the call. The best part - you are in complete control of the attestation level of the From number.

By setting up a Business Profile and adding your selected Phone Numbers to the SHAKEN/STIR TrustProduct, you can enjoy A level attestation on all outbound calls using those Phone Numbers.

Using a Verified CallerID as the From number on outbound calls will result in B level attestation.

Using a non verified Caller ID, or a Twilio Phone Number not added to a SHAKEN/STIR Trust Product will result in a C level attestation.

Bottom line - if you want high deliverability of your Outbound API calls, use SHAKEN/STIR.

Inbound Calls

Calls terminating on Twilio’s platform use the From number assigned by the originating carrier.

In your call logs, the From number is assigned by the originating carrier, and the To number is your Twilio Inbound number. When a call comes in to your Twilio number, we make a webhook to the defined URL. As part of this HTTP request, we include the StirVerstat parameter, 

Inbound call in Twilio showing StirVerstat parameter and B Attestation

Caller ID Attestation is still being rolled out, so there may be cases where the StirVerstat is missing or undefined. These cases should diminish over time.

We also send the CallToken parameter, which includes the actual jwt token containing the signed attestation information. This can be decoded with various libraries, or on jwt.io.

Decoding a token with jwt.io

Let’s Build an App

This is where the fun begins!

Man on a phone

Now that we know what SHAKEN/STIR is and how it works with Twilio, we should build something neat!

Let’s leverage the StirVerstat parameter to route the inbound calls in interesting ways using some super powerful Programmable Voice features.

Mobile providers use attestation as one metric to apply ‘nuisance labeling’, or to block calls on behalf of their subscribers.

Scam Likely screenshot from a phone

Why can’t you do something similar with your Twilio Programmable Voice app?

Prerequisites

All of the Functions used here can be found on my github.

Disclaimer: Your app and use case will have different requirements – don’t just copypasta this code into prod. These examples are only meant to demonstrate the power of combining Twilio’s diverse programmable call routing capabilities with the cutting edge SHAKEN/STIR attestation.

If you want to follow along, you will also need to perform a few steps:

Example folder structure for SHAKEN/STIR tests
  • Purchase a Phone Number and point it at the inbound-gateway.js Function URL.
  • While it is not required for inbound calls, I recommend onboarding to TrustHub and going through these steps to set up SHAKEN/STIR functionality for outbound calls.

Inbound calls trigger a webhook to the inbound-gateway Function. The StirVerstat value determines the ‘route’ to take, which then triggers ‘callbacks’.

In this scenario, I chose to route differently for every attestation level simply to demonstrate the power of Programmable Voice. In your case, you may decide another approach is more appropriate or practical.

And with that, we can begin.

Attestation Level Routing

To implement these call flows based on the Attestation level of the inbound caller, we must first create a ‘router’ Function which inspects the StirVerstat parameter.

Using a switch statement, we can check the parameter against our known list of values and have fine-grained control over each possible scenario.  

<Redirect> is one of my favorite TwiML verbs. It is simple, and powerful. You might think of this router function like an API gateway in a microservice architecture. The incoming requests are analyzed and forwarded according to the payload. Without <Redirect>, all of this logic would need to be in one Function.

Call Forwarding

If the caller is stamped with A attestation, then we know the caller id is trusted. Let’s just send them straight through to our cell phone - no action required. A simple <Dial> is all you need.

Keep in mind, the CallerID of the subsequent outbound <Dial> call will be the From number of the originating inbound call. You can override this by providing the CallerId attribute.

twiml
<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Dial>415-123-4567</Dial>
</Response>

Call Screening

‘It’s all your fault. I screen my phone calls’ - Gwen Stefani

We’ve all been there. But Twilio wasn’t around when Gwen and Tony wrote “Spiderwebs”, so their idea of call screening was probably "let the answering machine get it, and I’ll pick it up if they sound cool."

Today, Gwen Stefani is still writing bangers, but we have Twilio to help us with call screening. In this example, if the call is stamped with B attestation, the recipient will hear a recording of the caller’s name before they are connected.

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say> Hello. Please record your name. </Say>
    <Record action="myapp.com/callbacks/record-action" recordingStatusCallback="myapp.com/callbacks/record-status" />
</Response>

The inbound caller will hear the <Say> message and will be prompted to record a message notifying the recipient who is calling.

As soon as the caller is done leaving a message, the caller will hear hold music while the recording is processed and the call is forwarded.

As soon as the recording is ready, the call is updated with a <Dial>. The new RecordingURL is populated as the <Number> URL. When the recipient picks up, they will hear the recording and can decide to hang up or wait to be connected.

 

Voicemail

If the Attestation level of the inbound call is C, we can send them straight to a voicemail program. This Function returns a <Record> verb, and then sends a SMS to a number of your choice with a link to the recording file.

/**
 * Send the caller direct to voicemail, then notify me via SMS
 */

exports.handler = function(context, event, callback) {

  if (event.RecordingUrl) {
    console.log('this is the action callback');
    const twilioClient = context.getTwilioClient();
    const msg_from = event.To;
    const msg_to = context.PERSONAL_CELL;
    const msg_body = `You have a new voicemail from ${event.From} - ${event.RecordingUrl}`
    
    twilioClient.messages
      .create({ to: msg_to, from: msg_from, body: msg_body })
      .then((result) => {
        console.log('Created message using callback');
        console.log(result.sid);
        return callback();
      })
      .catch((error) => {
        console.error(error);
        return callback(error);
      });

  }
  else {
    console.log('This is the initial TwiML fetch...')
    let twiml = new Twilio.twiml.VoiceResponse();
    twiml.say('Thank you for calling. I am not available right now. Please leave a message after the tone. ');
    twiml.record({
    maxLength: 20,
    finishOnKey: '*'
  });

  twiml.say('I did not receive a recording. Good bye.');
  return callback(null, twiml);
  }
};

By taking advantage of a little known feature of the <Record> verb, we can make this single Function dual purpose. 

If you do not provide a <Record> action URL, then Twilio will hit the same document with the action callback.

The RecordingURL parameter is not present in the initial TwiML fetch, so we return <Record>. 

Whereas, the presence of RecordingURL in the request indicates the action callback, so we send an SMS with the link. 

Human Detection

Human Detection is similar in logic to Answering Machine Detection in that we want to determine if the caller is a human or a machine. In the context of outbound calls, Answering Machine detection makes a lot of sense. For example, we might want to use a different call flow if a human picks up vs. a machine. In the context of inbound calls, Human Detection is more useful to root out robocalls.

As mentioned before, the StirVerstat parameter may be missing, or have a value like “Validation-Failed” or “No-TN-Validation”. In these cases, we will use Human Detection.

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Gather action='/callbacks/gather-action' num_digits='2'>
        <Say>To prove you are human, please enter the number after nine.</Say>
    </Gather>
    <Say>We didn't receive any d t m f input. Goodbye</Say>
</Response>

Before the call is bridged, the caller will hear a <Say> message asking them to solve a captcha or puzzle.

In this example, we ask the caller to “Enter the number that comes after 9”

This can be anything, though - the harder for computers, the better.

After the user enters digits, Twilio makes a request to the <Gather> action url.

/**
* <Gather> action callback
* https://www.twilio.com/docs/voice/twiml/gather#action
*
* if the digit doesn't match, hangup. otherwise, send them through.
*/

exports.handler = function(context, event, callback) {
 let twiml = new Twilio.twiml.VoiceResponse();
 if (event.Digits != '10') {
   twiml.hangup()
 }
 else {
   twiml.dial(context.PERSONAL_CELL)
 }
 return callback(null, twiml);
};

If the correct digits are not entered, the call is ended using <Hangup>. Otherwise, we use a <Dial> to forward the call to our cell.

You may decide to include these in another route, or just log them for visibility. Either way, SHAKEN/STIR rollout is still ongoing, so a missing or unexpected StirVerstat value is no cause for immediate concern.

Conclusion

If you were building along, you can now call your number and test it out! You may need to adjust the switch statement in the router function if you don't have access to enough From numbers to test each path. 

Either way, you are now one step closer to becoming a SHAKEN/STIR aficionado! Whether you are trying to improve call deliverability, dynamically route incoming calls, or just analyze what types of numbers are calling your app, then Twilio has you covered with Trusted Calling.

We can’t wait to see what you STIR up!

Matt Coser is a Senior Field Security Engineer at Twilio. His focus is on telecom security, and empowering Twilio’s customers to build safely. Hit him up on linkedin to connect and discuss more.