Handling Payment Webhooks with Azure Functions
A few years back I created Skype Voice Changer Pro which I sold online, using Paddle as my payment provider. Whenever I make a sale (which isn’t too often these days thanks to an issue with recent versions of Skype), I get notified via a webhook. On receiving that webhook, I need to generate a license file and email it to the customer.
Azure Functions are perfect for this scenario. I can quickly create a secure webhook to handle the callback from Paddle, post a message onto a queue to trigger license generation, and then another queue to trigger sending an email.
Let’s see how we can set this up.
First of all, I need to create a new Azure Function, which I’ll create as a generic C# webhook:
The first thing I needed to do for my webhook, was edit the function.json
file to remove the “webHookType
” setting from the input httpTrigger
. By default this will be set to “genericJson
”, but that means we can only accept webhooks with JSON in their body. Paddle’s webhook comes in as x-www-form-urlencoded
content, so removing the webHookType
setting allows us to receive the HTTP request.
Now in our run.csx
file we can use ReadAsFormDataAsync
to get access to the form parameters.
Next, we need to validate the order. Azure Functions has got built-in webhook validation for GitHub and Slack, but not for Paddle, so we must do this ourselves. This is done using a shared secret which we can set in the App Service configuration and access through the ConfigurationManager
in the same way you would with a regular web app.
If the order is valid, for now let’s just respond saying thank you. Paddle will include this text in their confirmation email to the customer.
public static async Task<object> Run(HttpRequestMessage req, TraceWriter log)
{
var formData = await req.Content.ReadAsFormDataAsync();
var orderId = formData["p_order_id"];
var customerEmail = formData["customer_email"];
var messageId = formData["message_id"];
var customerName = formData["customer_name"];
log.Info($"Received {orderId}");
var sharedSecret = ConfigurationManager.ConnectionStrings["PaddleSharedSecret"].ConnectionString;
if (!ValidateOrder(sharedSecret, customerEmail, messageId, log))
{
log.Info($"Failed to Validate!");
return req.CreateResponse(HttpStatusCode.Forbidden, new {
error = $"Invalid message id"
});
}
return req.CreateResponse(HttpStatusCode.OK, new {
greeting = $"Thank you for order {orderId}!"
});
}
And here’s the C# code to validate a Paddle webhook:
public static bool ValidateOrder(string sharedSecret, string customerEmail, string messageId, TraceWriter log)
{
if (customerEmail == null || messageId == null)
{
log.Warning("Missing email or message id");
return false;
}
var input = HttpUtility.UrlEncode(customerEmail + sharedSecret);
var md5 = System.Security.Cryptography.MD5.Create();
byte[] inputBytes = Encoding.ASCII.GetBytes(input);
byte[] hash = md5.ComputeHash(inputBytes);
var sb = new StringBuilder();
foreach (byte t in hash)
{
sb.Append(t.ToString("x2"));
}
var expectedId = sb.ToString();
var success = (expectedId == messageId);
if (!success)
{
log.Warning($"Expected {expectedId}, got {messageId}");
}
return success;
}
Now the only thing left to do is to trigger the license generation and email, which we’ll do by posting a message to a queue. This is preferable to doing everything there and then in the webhook as queues allow our webhook to respond quickly and give us retrying if the email service is temporarily down. Breaking the process down into three small loosely coupled pieces will also give us maintainability and testability benefits.
We support send a message to a queue by going to the “Integrate” section in the portal and adding a new output binding of type Azure Storage Queue. This is the easiest to set up as there’s already a storage account associated with your function app you can use called “AzureWebJobsStorage” (although arguably you should create your own to keep your application data separate from the Azure Functions runtime’s data which resides in that storage account).
I’ll call my queue “orders”, and Azure Functions will automatically create it for me.
To send the message to the queue there are a number of options but I chose to create a strongly typed class “OrderInfo
” and use a IAsyncCollector<T>
parameter type for binding. This has the advantage of working with async functions (which mine is), but also supports sending 0 or more messages to the queue. We won’t be generating a license if the webhook is invalid so this is handy.
Here’s the key bits of the updated function:
public class OrderInfo
{
public string OrderId { get; set; }
public string CustomerEmail { get; set; }
public string CustomerName { get; set; }
public string LicenseDownloadCode { get; set; }
}
public static async Task<object> Run(HttpRequestMessage req, IAsyncCollector<OrderInfo> outputQueueItem, TraceWriter log)
{
// ... order validation here
// send on to the queue to generate license
var orderInfo = new OrderInfo {
OrderId = orderId,
CustomerEmail = customerEmail,
CustomerName = customerName,
LicenseDownloadCode = licenceDownloadCode,
};
await outputQueueItem.AddAsync(orderInfo);
return req.CreateResponse(HttpStatusCode.OK, new {
greeting = $"Thank you for order {orderId}!"
});
}
As you can see it’s super easy to send the message, just call AddAsync
on the collector.
Finally, we need to handle messages in the queue. There’s a super feature in the portal where if you go to the Integrate tab for your function and select the queue output binding, there’s a button to set up a new function that is triggered by messages on that queue:
By clicking this, it will auto-fill in the bindings for my new function, giving me a new function all set up to read off the queue and log each message received:
Now it’s just a case of putting my license generation code into this function, as well as posting to another queue to trigger a third function which sends out the license email. Azure functions includes a built-in SendGrid binding which makes sending emails very easy (although I’m currently using a different service).
We can easily test our function using Postman (can’t use the portal in this case as it only sends JSON), and sure enough the webhook function is successful, and we can see in the logs for the license generation function that a message was indeed posted to the queue.
Using Azure Functions to handle webhooks is a big improvement from the quick and dirty code I originally created which simply did everything synchronously in a hidden API sat on my website. It meant my order webhook code was now coupled to the web server, which got in the way of me doing things like switching the website to use WordPress. With Azure functions I can move this webhook (and several others for things like letting users report errors from the app) out of my website into small loosely coupled functions.