Zero
Zero
Back

Sending SMS Text Messages from Azure Functions with Twilio

Does your app need to send text alerts to users? In this article, we'll show you how to send SMS text messages progammatically using Twilio's API.

Sam Magura

Sam Magura

A smart phone showing various messaging apps

It's common for web applications to give users multiple options for how they want to receive notifications, 2FA codes, .etc. In our previous post, we covered sending transactional email via Mailchimp's API. So now it's time to send some SMS text messages!

We'll be using Twilio  for this, since it is the leading platform for sending automated texts. We've already done several blog posts using AWS Lambda , so we're going to use Azure Functions  for a change. As usual, we'll write the code in TypeScript and store the Twilio API credentials in the Zero secrets manager. Then, we'll use the Zero TypeScript SDK  to fetch the credentials at runtime.

🔗 The full code for this example is available in the zerosecrets/examples  GitHub repository.

Some Background on Azure Functions

If you aren't familiar with Azure Functions, it is largely similar to AWS Lambda, but in Microsoft Azure rather than AWS. Like AWS Lambda, Azure Functions allows you to write a function in JavaScript, C#, Python, .etc and then bind that function to a trigger which will cause your function to run. We'll be using an HTTP-triggered function in this walkthrough, though you can also configure you function to be triggered when a new message is sent to an Azure Service Bus  queue (among many other things).

One powerful feature of Azure Functions which I don't think has an equivalent in AWS Lambda, is that you can choose whether to run your functions in a serverless computing model (via a Consumption Plan) or on an App Service plan with a fixed amount of vCPU and memory (via a Premium or Dedicated Plan). The Premium and Dedicated Plans make Azure Functions suitable for long-running and memory-intensive tasks that would exceed the limits of most functions-as-a service platforms. The differences between these plans are explained here  in the Microsoft Azure docs.

Singing Up for Twilio

Now that we've covered the background, let's begin the walkthrough.

Singing up for Twilio is easy and free. Click here  to create your account.

Once you've created your account, you'll be shown the Twilio Console. Click the "Get a US toll-free phone number" on this page to get a phone number to send text messages from. We'll pass this phone number to our Azure Function as an environment variable in a future step.

At the bottom of the page you'll find your Account SID and Auth Token — these are the credentials you'll need to call the Twilio API. We will store the API credentials in Zero, so sign in to Zero and create a new project. You'll only be shown the Zero token once, so save it somewhere on your local disk. Then, click the "Create new secret" button, select Twilio from the dropdown, and paste in the Account SID and Auth Token.

The Account SID and Auth Token in the Twilio Console

Getting Set Up for Azure Functions Development

It's almost time to begin coding — but first, we need to install some tools which are needed for Azure Functions development.

🔗 If anything in this walkthrough is unclear, feel free to refer to this official quickstart tutorial  in the Azure docs, which my guide is based on.

To create and deploy an Azure Functions project, you'll need:

  1. An account on Microsoft Azure. Sign up for free here. 
  2. The Azure Functions Core Tools .
  3. Either the Azure CLI  or Azure PowerShell . We'll be using the Azure CLI in this article since it should feel more familiar to developers who work with Linux and shell scripting.

Then, follow the steps under "Prequisite check" here  to verify that everything is set up properly.

Setting up the Azure Functions Project

You can initialize a new TypeScript Azure Functions project with the command

1
func init TwilioAzureFunctions --typescript
shell

This will create a directory containing a package.json, a tsconfig.json, and some JSON configuration files that are specific to Azure Functions. cd into the new directory and run npm install.

Next, we'll use the Azure Functions Core Tools to scaffold an HTTP-triggered function for us:

1
func new --name SendSms --template "HTTP trigger" --authlevel "anonymous"
shell

You can check out the code that got generated by opening SendSms/index.ts. I updated the "methods" array in my function.json to only include "post", since GET requests should not trigger side effects like sending a text message.

Let's run the project to confirm that our function is able to respond to HTTP calls. To do this, execute

1
npm start
shell

followed by

1
curl -X POST http://localhost:7071/api/SendSms
shell

This should print the message returned by the function to your console.

Initializing the Twilio API Client

Now let's update our function to send an SMS text message when it receives an HTTP POST request. To do this, we'll leverage the Twilio npm package  and the Zero TypeScript SDK . You can install these with:

1
npm install twilio @zerosecrets/zero
shell

To be able to import the twilio package in our code, we need to add the following line to the compilerOptions key in tsconfig.json:

1
    "esModuleInterop": true
json

Unless you're working with a legacy TypeScript codebase, you should always have esModuleInterop enabled. Without it, you'll get compilation errors when importing from many popular JS libraries, even if your import statement exactly matches what is shown in the library's README.

In a real application, you would likely have multiple functions that need to send text messages. So it makes sense to initialize the Twilio API client in a utility function that can used throughout the codebase. This function will use the Zero SDK to fetch the Account SID and Auth Token from the cloud, and then pass those in to the constructor of the Twilio client. I placed the code for this in a file called utils/getTwilioClient.ts. Here it is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {Twilio} from 'twilio'

import {zero} from '@zerosecrets/zero'

let twilio: Twilio | undefined

export async function getTwilioClient(): Promise<Twilio> {
  // Reuse the same Twilio client if one has already been created, so that we
  // don't call Zero on every request
  if (twilio) {
    return twilio
  }

  if (!process.env.ZERO_TOKEN) {
    throw new Error('Did you forget to set the ZERO_TOKEN environment variable?')
  }

  const secrets = await zero({
    token: process.env.ZERO_TOKEN,
    pick: ['twilio'],
  }).fetch()

  if (!secrets.twilio) {
    throw new Error('Did not receive any secrets for Twilio.')
  }

  twilio = new Twilio(secrets.twilio.account_sid, secrets.twilio.auth_token)

  return twilio
}
typescript

Updating the Function Code

Let's return to the code for our main function, which is in SendSms/index.ts. The core piece of code which actually sends the SMS text message is

1
2
3
4
5
await twilio.messages.create({
  from: twilioNumber,
  to: myNumber,
  body: '(Your text message here)',
})
typescript

To make this work, we just need to assemble all the pieces. First, we'll set the twilioNumber and myNumber variables from environment variables. Then, we'll get a Twilio API client using the getTwilioClient function from the previous section. Finally, we call twilio.messages.create and return an HTTP response with the message's ID and status. This response data is just to help with debugging in case something goes wrong.

All together, the code for this is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
  const twilioNumber = process.env.TwilioNumber
  const myNumber = process.env.MyNumber

  if (!twilioNumber) {
    throw new Error('TwilioNumber environment variable not set.')
  }
  if (!myNumber) {
    throw new Error('MyNumber environment variable not set.')
  }

  const twilio = await getTwilioClient()

  const message = await twilio.messages.create({
    from: twilioNumber,
    to: myNumber,
    body: 'Hello from Twilio & Zero',
  })

  context.res = {
    body: `Message SID: ${message.sid}\nMessage status: ${message.status}`,
  }
}
typescript

Before running the project, let's set the TwilioNumber and MyNumber variables in local.settings.json. You could also set these variables when running npm start — though putting them in local.settings.json can be more convenient when you're restarting the application frequently during development.

The TwilioNumber and MyNumber variables should be placed under the "Values" key as follows. For MyNumber, you should enter your actual phone number.

1
2
3
4
5
6
7
8
9
10
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsStorage": "",

    "TwilioNumber": "+183355555555",
    "MyNumber": "+15555555555"
  }
}
json

Testing it Out

You can run the Azure Functions project locally via

1
ZERO_TOKEN='YOUR-ZERO-TOKEN' npm start
shell

Then issue a POST request to the function with curl like we did before:

1
curl -X POST http://localhost:7071/api/SendSms
shell

If it worked, you'll receive a text message!

Text message successfully received.

Deploying to Azure

The only thing left to do is to deploy our code to Azure. We'll create the Azure resources through the CLI, but you can also do most of these steps through the Azure Portal.

  1. Create a resource group:

    1
    az group create --name TwilioAzureFunctions-rg --location <REGION>
    
    shell

    Replace <REGION> with the name of an Azure region, like eastus2.

  2. Azure Function Apps require a storage account. You can create one with:

    1
    az storage account create --name <STORAGE_NAME> --location <REGION> --resource-group TwilioAzureFunctions-rg --sku Standard_LRS
    
    shell

    The storage account name must be globally unique and consist of lowercase letters and numbers only.

  3. Create a Function App with:

    1
    az functionapp create --resource-group TwilioAzureFunctions-rg --consumption-plan-location <REGION> --runtime node --runtime-version 18 -functions-version 4 --name <APP_NAME> --storage-account <STORAGE_NAME>
    
    shell

    The Function App's name is used in its URL, so it needs to be globally unique.

  4. Build your Azure Functions project for production by running

    1
    npm run build
    
    shell
  5. Now use the Azure Functions Core Tools to deploy your local project to the Function App:

    1
    func azure functionapp publish <APP_NAME>
    
    shell
  6. The local.settings.json is not deployed with your app, so we need to add the TwilioNumber and MyNumber variables as app settings in the Function App's configuration. You can do this by navigating to the Function App in Azure Portal  and clicking "Configuration" in the menu on the left. Add the two variables via the "New application setting" button and click "Save".

The TwilioNumber and MyNumber settings in the Configuration tab in Azure Portal

To test that the function still works now that it's hosted in Azure, run

1
curl -X POST https://<APP_NAME>.azurewebsites.net/api/SendSms
shell

where <APP_NAME> is the name of your Function App.

Cleaning Up

If you created any Azure resources, be sure to clean them up when you are done. You can do this by running

1
az group delete --name TwilioAzureFunctions-rg
shell

Next Steps

In this article, we created an Azure Functions application that sends a SMS text message via Twilio, with the Twilio API credentials fetched at runtime from the Zero secrets manager. At this point, there's two areas in which you can continue your exploration. The first would be to delve deeper into the APIs provided by Twilio. For example, you may be interested in creating an interactive chat experience via Twilio Conversations , or adding voice notifications to your app with Twilio Voice .

Another direction you can explore is building out your application's infrastructure in Azure. Azure Functions pairs very well with Azure Static Websites , which is great for hosting single-page application frontends. For persistence, Azure SQL  is the simplest option, while Azure Cosmos DB  offers better scalability for apps that see extremely heavy usage.