SMS Fallback to Voice Notifications

August 18, 2022
Written by
Reviewed by

SMS Fallback to Voice Notifications

Did you know that ~40% of the calls through Twilio are to a phone that can’t receive text messages? How many folks haven’t opted into your messaging campaigns that you have phone numbers for?

Voice notifications enable you to communicate with everyone that gives you a phone number, which helps drive up appointments, reservations, or action. Even if users don’t answer the phone, visual voicemail will transcribe your voice notification and provide a similar result as an SMS. This post will show you how to reach the folks you’re not reaching today with Programmable Voice.

Example of a voicemail automatically transcribed

Prerequisites

To follow this tutorial, you need the following items:

Please reference the companion repo on Github.

Use Twilio Functions to send voice notifications

Write a Function to send SMS

If you have already made calls to Twilio’s Programmable SMS API to send your customer notifications, you can go ahead and leverage that existing code to send an SMS. If not, you can follow along with our sample.

Navigate to Functions and Assets -> Services in the Twilio Console. Click “Create Service” button.

Create New Service

Enter “voice-notifications” in the Service Name input and click the “Next” button.

Name your service

Click the “Add +” button and select “Add Function” from the drop-down. Replace “path_1” with “sendSMS”.

Add a new function

Click the down arrow to the right of the word “Protected” and select “Public” from the drop-down.

Make that function public

We’re going to write a simple Twilio Function to send an SMS based on 3 parameters: From, To, and Body. These values are stored in the event object, which contains request parameters and headers, and we access them from the event parameter. We will leverage Axios to make an HTTP request to our other functions, so let’s add that in addition to the Twilio client.

const axios = require('axios');

exports.handler = async function (context, event, callback) {
  const twilioClient = context.getTwilioClient();

  // Query parameters or values sent in a POST body can be accessed from `event`
  const from = event.From;
  const to = event.To;
  const body = event.Body;
};

Next let’s use the Twilio client to send an SMS message by adding the following code directly after the const body variable:

twilioClient.messages
    .create({ body, to, from})
    .then((message) => {
      console.log('SMS successfully sent');
      console.log(message.sid);
      return callback(null, `Success! Message SID: ${message.sid}`);
    })
    .catch((error) => {
      console.log("errorCode:", error.code);
      return callback(error);         
    });

Set dependencies

Click on the “Dependencies” link under the Settings pane. Type “axios” into the MODULE box and “^0.27.2” in the VERSION text box. Then click the “Add” button.  

Add another dependency by entering “twilio” into  the MODULE box and “3.56” into the VERSION box. Then click the “Add” button.

add node dependencies

Deploy Functions

Click the blue “Deploy All” button in the bottom left corner.

Copy the URL of the /sendSMS function by clicking the three dots to the right of the function name and selecting “Copy URL”.

Copy function URL

Example: https://voice-notifications-1234.twil.io/sendSMS

Test your function to make sure you're able to send an SMS replacing <copied_url> with the link copied above.

curl --location --request POST '<copied_url>' \
--header 'Content-Type: application/json' \
--data-raw '{
    "From": "<your_twilio_number>",
    "To": "<your_personal_phone_number>",
    "Body": "Good news, your table is ready! Please proceed to the host stand now."
}'

Notice the link in the above command…

Write Function to send voice notifications

Now that we have sent an SMS, we will write another Twilio Function named /sendVoiceNotification to send a voice notification.

Add new function

Again we will need to change this function from Protected to Public. We will use the same 3 parameters, From, To, and Body. Using the Body parameter, we will create TwiML, which will speak the message provided to the function.

exports.handler = function (context, event, callback) {
    const from = event.From;
    const to = event.To;
    const body = event.Body;
    const twiml = `<Response><Say voice="Polly.Kimberly-Neural">${body}</Say></Response>`;

    const twilioClient = context.getTwilioClient();
};

Then we will create a Programmable Voice call using the to, from, and twiml variables. The call will use Answering Machine Detection to ensure if the customer doesn’t answer, Twilio will leave them a voicemail. Add this code below directly below the twilioClient variable.

    twilioClient.calls
        .create({
            twiml: twiml,
            to: to,
            from: from,
            machineDetection: 'DetectMessageEnd'
        })
        .then((call) => {
            console.log('Call successfully placed');
            console.log(call.sid);
            return callback(null, `Success! Call SID: ${call.sid}`);
        })
        .catch((error) => {
            console.error(error);
            return callback(error);
        });

Handle undeliverable SMS

Next we need to write the logic to handle a scenario where the text message was undeliverable. There are two primary ways to get notified when an SMS is undeliverable, Event Streams and Status Callbacks.

Leveraging Event Streams allows you to subscribe to a specified list of event types, so for this we can just subscribe to “Undeliverable SMS” events. The main advantage to using Event Streams is that you do not have to modify your existing code. Subscribing to Event Streams enables you to receive SMS undeliverable status events from your entire account. The drawback to this approach is that it may require additional logic if you want to take different actions for different messages. For example, you may want to send a simple voice notification to tell a customer that their table is ready, but you might want to leverage an IVR to notify them of a change to their reservation so they can reschedule.

Event Streams allows you to create Webhook, Amazon Kinesis, or Segment Sink types. In this example, we will use a simple Webhook sink to call another Twilio Function.

Click the “Add +” button on the Functions page and name the new function /eventStreamWebhook. Again make this function Public.

After adding new functions

Paste the following code into the new function.

const axios = require('axios');

exports.handler = async (context, event, callback) => {
  const twilioClient = context.getTwilioClient();

  const smsEvent = event['0'].data;
  const smsStatus = smsEvent.messageStatus;
  const smsTwilioErrors = {
    30003: 'Unreachable destination handset',
    30004: 'Message blocked',
    30005: 'Unknown destination handset',
    30006: 'Landline or unreachable carrier',
    30007: 'Message filtered',
    30008: 'Unknown error',
    30017: 'Carrier network congestion',
    30018: 'Destination carrier requires sender ID pre-registration',
    30022: 'US A2P 10DLC - Rate Limits Exceeded',
    30023: 'US A2P 10DLC - Daily Message Cap Reached carrier requires sender ID pre-registration',
    30027: 'US A2P 10DLC - T-Mobile Daily Message Limit Reached',
  };

  console.log(
    'Event Streams Undelivered Event Received\n messageStatus: ',
    smsStatus,
  );

  // Only parse undelivered calls in this function
  if (smsEvent.messageStatus !== 'UNDELIVERED') {
    console.log("not UNDELIVERED");
    return callback(null, 'Callback Received');
  }

  // Checking to see if relevant voice call
  if (!smsTwilioErrors[smsEvent.errorCode]) {
    console.log("not known error code: " + smsEvent.errorCode);
    return callback(null, 'Callback Received');
  }

  const message = await twilioClient.messages(smsEvent.messageSid).fetch();

  // send a voice notification with that same body
  const sendVoiceCall = await axios({
    method: 'POST',
    url: 'https://' + context.DOMAIN_NAME + '/sendVoiceNotification',
    data: {
      From: smsEvent.from,
      To: smsEvent.to,
      Body: message.body,
    },
  });

  console.log(sendVoiceCall.status);
  return callback(null, '');
};

Click the ”Deploy All” button at the button of the page to build and deploy our latest changes to the Twilio Functions serverless environment.

Copy the URL for your /eventStreamWebhook function, you will use it in the next step!

Setting up Event Streams to a webhook destination

In this blog post, we are going to use a Webhook destination, but any of the destinations will work to collect SMS undelivered events. If you prefer to use Segment or Kinesis please refer to the Event Streams documentation.

Create a Sink Instance

Navigate to “Explore Products” in the Twilio Console and click on Event Streams under the Developer tools section. Then click Manage and click “Create new sink”.

Create a new sink

Provide a description for the new Sink and select the Webhook sink type.

Configure new sink

Click “Next step” and paste the /eventStreamWebhook URL into the Destination field. Select “POST” and “False” for the next two options then click “Finish”.

Configure webhook sink

In the resulting pop-up, click “Create Subscription” and then continue to the next section.

Create subscription pop-up

Subscribe to undelivered SMS events

An Event Stream sink needs to have one or more subscriptions to event types. This will let Twilio Event Streams know which event types your application is interested in receiving. In this case, we just want to know when an SMS message is undelivered.

Provide a description for this new subscription.

Sink subscription description

Scroll down to the “Product groups” section and click “Messaging” and then scroll to the bottom of the list and select the latest “Schema version” for the “Undelivered” event type. At the time of writing, the latest version was “3”.

Sink subscription Undelivered message schema

Click “Create Subscription” at the bottom, and you are done building your automated system for sending Voice Notifications for failed SMS!

Test it out!

To mimic a cell phone subscriber who cannot receive SMS messages, we can purchase a Twilio Number and set the Incoming Message Webhook to a blank value. Any SMS messages to that number will result in a 30008 error code.

Purchase a Twilio Phone Number

Navigate in the console to Phone Numbers -> Buy a number and then click the “Buy” button next to a number of your choice. For detailed instructions, please see this article.

Buy a second phone number

Write this number down, you will swap this number in the example below as the <twilio_number_to> variable.

Create a TwiML Bin

In the Twilio Console, navigate to TwiML Bins (either pinned to the menu or through clicking “Explore Products”) -> My TwiML Bins and then click the “+” button to add a new TwiML Bin. Provide the TwiML Bin with a name like “Forward Voice to my cell phone” and then paste the following code into the TWIML field, replacing the phone number with your own phone number.

    <?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Dial> +19192634335 </Dial>
</Response>

Create a new TwiML Bin

Click “Create” at the bottom of the screen.

Configure the new Twilio Phone Number

Navigate in the console to Phone Numbers -> Active Numbers and click on the number you just purchased.

Scroll down to the Voice configuration and select the “TwiML Bin” option in the “A CALL COMES IN” drop down and then select the “Forward Voice to cell phone” TwiML bin we created earlier.

Configure Voice for Phone Number

Scroll down to the Messaging configuration and select the “Webhook” option in the “A MESSAGE COMES IN” drop down and then leave the text input blank. This will signal to Twilio to reject any upcoming SMS and return a 30008 error code to the originating Twilio Programmable Messaging API status callback.

Configure Messaging for Phone Number

Click “Save” at the bottom of the page.

Use this number as the <twilio_number_to> field below.

Make a call to your /sendSMS function

Now we will use the functions you wrote earlier to send an SMS to your new Twilio number. Use the following curl command to invoke your new function. Insert the Twilio Number you just purchased as the <twilio_number_to> field. Insert the original Twilio Number provided with your account (or any other SMS enabled Twilio Number in your account) in the <original_twilio_number> field. 

curl --location --request POST 'https://<your_function_domain>/sendSMS' \
--header 'Content-Type: application/json' \
--data-raw '{
    "From": "<original_twilio_number>",
    "To": "<twilio_number_to>",
    "Body": "Good news, your table is ready! Please proceed to the host stand."
}'

You should see a Success! Message SID: in the terminal after running this curl command and then you should receive a voice call from your Twilio Number that will speak the original message to you. Try answering the call and then try again to see what happens when you don’t answer the incoming call.

 

Handle opt-outs

Some users may opt-out of SMS messages which might have unintended consequences for their experience with your brand. For example, if a user does not realize that SMS messages are used for notification that their to-go order is ready, missing that notification might cause them to have a poor customer experience. In cases such as this, you can fall back to a voice notification automatically.

We will add some logic to our /sendSMS Function to detect when a 21610 error is thrown, indicating that a user has opted out of messages from that number. Ideally, this would never occur since we recommend developers listen to incoming message webhooks and update their backend database when a user replies with an opt-out keyword. In that case, you would add this logic inside your application to check if the user was unsubscribed before sending, and whether the voice notification should be sent instead.

Insert this code inside the catch block at the end of the /sendSMS function to replace the return callback.

if (error.code != null && error.code == '21610') {
        // for unsubscribes send voice notification
        axios
          .post('https://' + context.DOMAIN_NAME + '/sendVoiceNotification', {
            From: from,
            To: to,
            Body: body
          })
          .then(res => {
            console.log(`statusCode: ${res.status}`)
            return callback("", "Tried to send to unsubscribed user, call placed instead");
          })
          .catch(voiceNotiferror => {
            // Error with both SMS and Voice, log both and return original SMS error to API request
            console.error("Error with sending SMS:", error);
            console.error("Error with sending Call:", voiceNotiferror)
            return callback(error);
          })
      } else {
        // else log the error and return 500 to API request
        console.log("other error other and 21610");
        return callback(error);
      }
    });

So you’re resulting code should look like:

const axios = require('axios');

exports.handler = async function (context, event, callback) {
  const twilioClient = context.getTwilioClient();

  // Query parameters or values sent in a POST body can be accessed from `event`
  const from = event.From;
  const to = event.To;
  const body = event.Body;

  twilioClient.messages
    .create({ body, to, from})
    .then((message) => {
      console.log('SMS successfully sent');
      console.log(message.sid);
      return callback(null, `Success! Message SID: ${message.sid}`);
    })
    .catch((error) => {
      if (error.code != null && error.code == '21610') {
        // for unsubscribes send voice notification
        axios
          .post('https://' + context.DOMAIN_NAME + '/sendVoiceNotification', {
            From: from,
            To: to,
            Body: body
          })
          .then(res => {
            console.log(`statusCode: ${res.status}`)
            return callback("", "Tried to send to unsubscribed user, call placed instead");
          })
          .catch(voiceNotiferror => {
            // Error with both SMS and Voice, log both and return original SMS error to API request
            console.error("Error with sending SMS:", error);
            console.error("Error with sending Call:", voiceNotiferror)
            return callback(error);
          })
      } else {
        // else log the error and return 500 to API request
        console.log("other error other and 21610");
        return callback(error);
      }
    });
};

To test this, use your personal cell phone and text “STOP” to your original Twilio Number (the one you have been using to send SMS messages from). Then call the /sendSMS function with that original Twilio Number as the From and your personal cell phone number as the To. You should not receive the SMS since you are opted-out but instead you should receive a voice notification with the same content.

 

Final considerations and resources

I’ll leave you with a few caveats to this solution you need to consider:

Enable your customers to press a digit or say something to be able to speak to a human, so you’re not sending the equivalent of a “no reply,” and your customers have to go elsewhere for service:

  • Best practice would be to direct them to the relevant contact center queue, e.g. rescheduling, or human best placed to deal with that inquiry e.g. restaurant front desk.
  • As you scale voice notifications, try and scale them alongside human capacity to answer the anticipated contact center volumes.
  • Use a Conversational IVR to capture caller intent and include this in your inbound contact center call.

If your message is sending URLs you should consider replacing the URL with an alternative. Some examples are:

  • Providing them a way to call in for more information
  • Allowing them to press 1 to receive the link via a 1-time message via a separate Messaging Service from your normal notifications
  • Allowing them to opt-in/resubscribe to SMS messages, and then sending the SMS with the URL.

Be aware of when you are sending messages with numbers. Here are a few scenarios where some massaging of the message is needed before sending the voice notification:

  • Detect when currencies are used and replace with the amount and spoken currency ($5.02 -> 5 dollars and 2 cents).
  • Insert spaces between digits for spoken numbers (order numbers etc.)
  • Convert time of day (5:30 PM would be “five thirty PM”)

Always use a Neural voice for voice notifications so that visual voicemail transcription will be more accurate.

If you want to specify different content for each SMS message you can leverage status callbacks to initiate the Voice Notification which will allow you to specify an alternative message. The downside to this approach is that you must edit each SMS API call to provide a specific status callback endpoint.

Final thoughts...

Even with Twilio’s unmatched SMS deliverability, there will ultimately be some customers you cannot reach via SMS, such as landlines. Using automatic fallback to voice notifications allows you to easily reach those customers and still provide a great customer experience without rearchitecting your customer engagement model. It can also offer a way to reach customers who have opted out of SMS but still need communications from your business to have a great experience.

Additional resources

The following reference resources will provide you with in-depth information on some of the topics mentioned in this post:

Mark is a Solutions Engineer working with Enterprise customers at Twilio. He has spent the last decade working with customers on everything from VoIP to enterprise integration patterns. In his spare time, he enjoys trying to keep his home automation running. He can be reached mvickstrom [at] twilio.com.