12/18/2020

Use Azure Functions to Receive Twilio Webhooks and persist in Blob Storage

By: Nicklas Møller Jepsen

How to receive Twilio webhooks using Azure Functions

There are a few ways to develop Azure Functions. In this post we are going to use Visual Studio to develop the function, but we could also have choosen to use the Azure Portal. Since I don't like to code in a browser, I prefer Visual Studio over the Portal!

There are a few steps involved, but it's actually super simple, and this didn't take much more than ½ an hour to get done, so read along.

Highlevel

  1. Create a new Azure Function project
  2. Setup a HTTP Binding
  3. Read the webhook data
  4. Persist to Azure Storage using outbound binding
  5. Deploy and done!

...ohh and don't forget to tell Twilio to send the webhooks to your new awesome Function.

Create and implement the Function

Begin by creating a new Azure Function in Visual Studio. You will be asked to define the trigger, selet Http trigger like so: Http Trigger

First thing to do is to configure the HttpTrigger attribute. Twilio will only POST to your function, so you can delete get from the triggers params methods. Since we will persist the webhook data to Azure Storage, we also want to create an output binding to a CloudBlobContainer, add the Blobattribute, pass the blobPath (which is the container name, point to a Connection defined in local.settings.json (more on that later). Your function signature should look like this:

public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    [Blob("twilio-webhooks", Connection = "AzureWebJobsStorage")] CloudBlobContainer outputContainer, ILogger log)

The Function code

Now we are going to do the following:

  1. Read the request body
  2. Parse Twilio's format
  3. Serialize as JSON
  4. Write the JSON to our Blob Container

Here's how to do that:

// Read the request body
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
log.LogDebug($"Request body: {requestBody}");

// Twilio sends form data in the body, the below parses this into JSON. Here's Twilio's doc: https://www.twilio.com/docs/usage/webhooks/sms-webhooks
var formValues = requestBody.Split('&')
    .Select(value => value.Split('='))
    .ToDictionary(pair => Uri.UnescapeDataString(pair[0]),
        pair => Uri.UnescapeDataString(pair[1]));
var jsonObj = JsonConvert.SerializeObject(formValues, Formatting.Indented);
log.LogDebug($"Request parsed to JSON: {JsonConvert.SerializeObject(jsonObj)}");

// Now persist in Azure Storage
// This can be omitted
await outputContainer.CreateIfNotExistsAsync();
// Create a blob name like: 2020-12-18 20-45-12 59A0962E-2AE9-498C-8B50-FE3F5935B556.json
var blobName = $"{DateTime.UtcNow:yyyy-MM-dd HH-mm-ss}-{Guid.NewGuid()}.json";
var cloudBlockBlob = outputContainer.GetBlockBlobReference(blobName);
await cloudBlockBlob.UploadTextAsync(jsonObj);

And finally, which is SUPER IMPORTANT return 200 OK to Twilio. If you don't do that in a timely manner or if your function returns an error, Twilio might stop sending you webhooks. I have implemented a pattern to handle this by adding a second layer of abstration and throwing an Azure Queue in between the receiving and processing part of this, but more on that in another post.

return new OkResult();

Here are all the pieces put nicely together:

public static class WebhookFunctions
{
    [FunctionName("TwilioWebhook")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
        [Blob("twilio-webhooks", Connection = "AzureWebJobsStorage")] CloudBlobContainer outputContainer, ILogger log)
    {
        log.LogInformation("TwilioWebhook trigger function processing a request.");

        // Read the request body
        var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        log.LogDebug($"Request body: {requestBody}");

        // Twilio sends form data in the body, the below parses this into JSON. Here's Twilio's doc: https://www.twilio.com/docs/usage/webhooks/sms-webhooks
        var formValues = requestBody.Split('&')
            .Select(value => value.Split('='))
            .ToDictionary(pair => Uri.UnescapeDataString(pair[0]),
                pair => Uri.UnescapeDataString(pair[1]));
        var jsonObj = JsonConvert.SerializeObject(formValues, Formatting.Indented);
        log.LogDebug($"Request parsed to JSON: {JsonConvert.SerializeObject(jsonObj)}");

        // Now persist in Azure Storage
        // This can be omitted
        await outputContainer.CreateIfNotExistsAsync();
        // Create a blob name like: 2020-12-18 20-45-12 59A0962E-2AE9-498C-8B50-FE3F5935B556.json
        var blobName = $"{DateTime.UtcNow:yyyy-MM-dd HH-mm-ss} {Guid.NewGuid()}.json";
        var cloudBlockBlob = outputContainer.GetBlockBlobReference(blobName);
        await cloudBlockBlob.UploadTextAsync(jsonObj);

        return new OkResult();
    }
}

Test the function

Before testing locally, please make sure to put the AzureWebJobsStorage config value in the local.settings.json, like this:

{
  "Values": {
    "AzureWebJobsStorage": "DefaultEndpointsProt..."
		
		...
		
	 }
}

Now it's time to test it out, hit F5 to debug your function and you should see something like this: Debuggin the function Simply fire up Fiddler or Postman, do a POST request to your functions url (localhost, some port, see the console window). Then, remember Twilio will send form encoded data, so you need to do the same when testing it. In Fiddler, that looks something like this: Fiddler

Let the function process the request and you should end up with something like this in Azure Storage Explorer: StorageExplorer

Deployment

It's simple to deploy the function, just remember to put the AzureWebJobsStorage in the App Settings in the Function app.

Configuring the webhook url in Twilio

After deployment, you can get the function url from the Azure portal, copy this url, go to your Twilio page and paste it in there to let Twilio know to send webhooks your way.

That's it, please let me know what you think in the comments, and feel free to ask questions, if you have any. Thanks for reading!