Handling webhooks is a very common pattern in backend development. You call a third party API, and the API immediately returns successfully. Then the third party does something nifty like sending a message, or using a neural network. and when it’s done, it calls your API with the results.
It’s a pretty straightforward pattern that makes a whole lot of sense on paper. But it can get complicated in practice. Once you’ve called the API, you’ve completely lost state. When the service calls your webhook, you need to figure out which of your entities it’s talking about. And then you need to recreate the state it was in before calling the API. This generally involves a lot of calls back to your database to recreate what you had seconds ago.
I’ve built countless webhook handlers over the years. I felt like I had optimized the webhook pattern as much as possible. And then I built one with Azure Durable Functions.
For webhooks, the human interaction pattern is what we’ll use. This pattern involves:
In this case, the external event will be a webhook. This recipe is going to involve sending an SMS message using the popular Twilio service. After sending a message, Twilio can respond back with the delivery status. For example, you may want to do some business logic if a message failed to send.
I’ve build Twilio handlers in many languages for many use cases. But Durable Functions make it the easiest by far. It usually goes something like this:
I’ve got all sorts of information about the person I’m about to send a message to. I send the message, mark down a unique message ID that Twilio gives me, and that’s it. Everything is gone. Now I wait.
Some time later (usually within seconds), Twilio hits my API with a unique message ID. This is generally in a completely separate part of the app. I use the ID to lookup the message, and now I’ve got to recreate everything I had before sending to Twilio. But I just had it! It may not seem like much, but at scale, all of these trips back to the data store really add up.
In Durable Functions, the webhook logic happens one line after sending the message. Let’s see how it works.
I’ll be running VS Code on a Mac, but this will work in any OS. We’ll start by using the Azure Functions extension in VS Code to create a new C# function project in an empty directory.
We’ll then need some packages. Run these commands to get the extensions for Durable Functions, Azure Table Storage, and Twilio.
dotnet add package Microsoft.Azure.WebJobs.Extensions.DurableTask --version 1.6.2
dotnet add package Microsoft.Azure.WebJobs.Extensions.Storage --version 3.0.1
dotnet add package Microsoft.Azure.WebJobs.Extensions.Twilio --version 3.0.0
Models directory and file called
MessageInput.cs in it. This will contain the properties needed to send a message:
We need some way to start the sending of a message. We’ll use an HTTP trigger. If you’re not familiar with Durable Functions, you can ignore the function parameters.
We read the body (as a
MessageInput object that we created earlier) and pass it to an orchestrator to start the workflow. Then we return with the orchestration status. That’s it! Now we need to create an orchestrator.
This is where the real magic happens. The entire workflow for sending a message and getting its response happens in five lines of code.
Each orchestration has a unique ID. This ID should generally be kept secret, so we’ll create a mapping that maps the orchestration ID to a unique ID. We’ll send the unique ID to the third party, and when they give it back to us, we’ll use it to lookup the orchestration ID.
We send the message, and then call
WaitForExternalEvent. This pauses the orchestration indefinitely until the
TwilioCallback event is fired. At this point, we’ll have the status and can continue with our business logic.
We’ll use Azure Table Storage (which Durable Functions use anyway) to save our mapping. Azure Functions support a Table Binding which allows you to pull data out of the database with little code.
We need a class to hold the mapping:
Table Storage needs a
PartitionKey and a
RowKey. We’ll save the
OrchestrationId and use the
RowKey to send to Twilio, and to find our orchestration again.
This function simply saves the mapping to Table Storage and returns the
RowKey for later use. We get to use the Table binding to specify which table we’re interested in in the function parameters.
Azure functions also support Twilio Bindings, which send SMS messages with little code. Here, we create a
CreateMessageOptions object and populate it with the inputted number.
StatusCallback, is the URL Twilio will hit to give us the message’s status. We include the
RowKey in the URL to lookup our mapping. We’ll create that route next.
The last thing to do is handle when Twilio responds back to our API with the status. This, of course, is just another HTTP triggered function. The function parameters aren’t much different than the last HTTP function.
The only difference is it uses the Table binding to lookup the ID that Twilio passed back. This returns a record with the orchestration ID, which means we can now raise an event to that orchestration.
If you’ll recall, the orchestrator is currently waiting for a
TwilioCallback event to fire, so let’s keep it waiting no more! We pass in the status and the orchestration can resume right where it left off.
Note: Azure functions currently do not support x-www-form-urlencoded responses, which is what Twilio provides.
ParseForm handles this for us. See the full code for details.
That’s it! Five functions to send messages and handle callbacks. Best of all, we can pick up right where we left off before we started waiting for the webhook. No more duplicate trips back to the database. It’s the cleanest way to implement webhooks that I’ve experienced.
Check out the full code or the video above for the full details!