Tech blog: Hooked on Webhooks?

SharePoint now has limited support for Webhooks. Our Technical Lead, Paul McGonigle, explores how they can be used, and whether they’re worth using.

New features get added to SharePoint Online regularly, and I find it’s worth staying on top of them so we can keep our solutions cutting-edge at CompanyNet. One of the new features I’ve been digging into recently is Webhooks. Webhooks are HTTP callbacks that are automatically triggered when something happens. At the moment, support for Webhooks in SharePoint is limited to SharePoint lists.

On first glance, they seemed like they might fill a void in the SharePoint online developer toolset. I took the opportunity to explore Webhooks with a real-world scenario, and found several oddities with working with them.

This blog post will detail the process I went through, which will hopefully be useful for anyone else looking into this technology.

Running custom code with Webhooks

The requirements for the real-world scenario were simple. We needed to run some custom code every time a task item was updated in a task list on any one of many sites. This code could interact with a variety of different platforms (SharePoint, SQL and Dynamics CRM).

My original plan had been to use the Webhook to add the notification to a service bus queue and get a WebJob to pick up the notifications and process them. After doing a bit more research, it turns out that, when adding a Webhook, SharePoint will try to verify the URL that you passed to it. It does this by passing a validation token to the URL, expecting a response with this validation token. If it doesn’t get the validation token back, you’ll receive an error message saying the URL validation failed. So, instead of adding the notification to a queue, I had to pass it to some code to check for the presence of the token before responding.

This meant a slight tweak was required to the architecture. I introduced an Azure Function to verify the message before adding it to the queue. I wanted to keep the Azure Function light in terms of verification, so all it does is check for the validation token – if it’s present, it returns it, otherwise it adds the message to the queue. The main content to my Azure Function is as follows:

[turn_to_code]
// parse query parameter

string validationToken = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, “validationToken”, true) == 0)
.Value;

// If a validation token is present, we need to respond within 5 seconds by
// returning the given validation token. This only happens when a new
// web hook is being added

if (validationToken != null)
{
log.Info($”Validation token {validationToken} received”);
var response = req.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(validationToken);
return response;
}

log.Info(DateTime.Now.ToUniversalTime().Ticks.ToString());
// Get request body
log.Info(“Request URL: ” + req.RequestUri);
string data = await req.Content.ReadAsStringAsync();
log.Info(“Body of request: ” + data);
outputSbMsg.Add(data);
log.Info(“Added message to queue”);

return req.CreateResponse(HttpStatusCode.Accepted, “Notification received”);
[/turn_to_code]

So now that I had the messages in my queue, I needed to start creating my WebJob. The WebJob’s first task is to get the relevant updated task items to see if they are completed (as opposed to just being updated). The message that gets added to the queue is an array of type WebhookNotification. I created the following object to match the message:

[turn_to_code]
internal class Notification
{
public WebhookNotification[] value { get; set; }
}
[/turn_to_code]

The message can then be converted into a Notification object:
[turn_to_code]
Stream stream = message.GetBody();
StreamReader reader = new StreamReader(stream);
string s = reader.ReadToEnd();
var notifications = Newtonsoft.Json.JsonConvert.DeserializeObject(s);
[/turn_to_code]

What changed?

Great – so I had the Webhook message, and could access all its properties. Now I just needed to understand what had changed, and do some business logic dependent on whether the task was completed. The WebhookNotification class makes the following properties available to us:

  • SubscriptionId
  • ClientState – this is an optional string that’s very useful if you want to use one URL to process several different Webhook subscriptions.
  • Expiration
  • Resource – this is the List ID, not the list item.
  • TenantId
  • SiteUrl – a relative URL to the site.
  • WebId

Looking at the properties passed there are a few areas to be careful around:

  1. None of the properties passed as part of the notification have a full absolute URL to the tenant. We are passed a Tenant ID and relative Site URL, but if you’re creating a multi-tenant application you’ll need to plan around this.
  2. The Webhook has an expiration on it, and you need to plan around monitoring your Webhooks for when they expire. As far as I can tell, the best solution is to have a scheduled WebJob that regularly checks and updates Webhooks for expiration.
  3. They don’t pass you the item ID for the item that has been updated. This is by far the most significant issue. Off course the Webhook will fire on overall list events, but surely a null value could be passed for the list item?

The solution I’ve seen suggested for the third point above is to use the GetChanges endpoint, which exists on the List. This endpoint can be queried using the ChangeQuery object, which has several properties that can be set to filter out certain changes (i.e. Item, List, Folder, File). However, this query is where we get to the next quirk of this approach. We should be setting the ChangeTokenStart on this query to time-box when we are getting changes from; two approaches are suggested here:

  1. Set the start time as the current time minus one minute. This approach will fall flat on its face if either the Webhook or the WebJob fail and try to re-run;
  2. Track the WebJob’s last successful Change Token and store it somewhere. Several examples here use Azure SQL for storage; to avoid additional costs I tried this approach using a SharePoint list – which seemed to work quite well.

Finally, we’ve got an array of changes whereby we can iterate through them to see if any are changes to an item:

[turn_to_code]
foreach (var c in changes)
{
if (c is ChangeItem)
{
lastChangeToken = c.ChangeToken.StringValue;
var change = c as ChangeItem;
if (change.ChangeType == ChangeType.Update)
{
[/turn_to_code]

At this stage, I thought I’d be able to get access to the List item through the change item and be able to check if the column “Status” was set to “Completed”. Unfortunately, the change item doesn’t contain this information – it does have a list item ID, and – as you might have worked out by now – this means another call to SharePoint to load the list item.

In conclusion, Webhooks aren’t bad; they’re just not great. If you do plan to use them, some of the downsides to consider are:

  • Webhooks require maintenance to monitor expiration.
  • If you’re targeting multi-tenant you will have to carefully consider your design.
  • There is no user interface out-of-the-box for Webhooks – adding them requires code.
  • The notification message from SharePoint doesn’t carry enough information.
  • Too many calls are required to SharePoint to know what has changed.

I’m going to continue watching Webhooks with interest to see if they become more useful, as with some improvements they could become a handy part of the SharePoint coder’s toolset.

Share this page