SharePoint webhooks enable developers to build applications that subscribe to receive notifications on specific events that occur in SharePoint. When an event is triggered, SharePoint sends an HTTP POST payload to the subscriber. Webhooks are easier to develop and consume than SharePoint remote event receivers because webhooks are regular HTTP services (web API).

This article shows how to set up and use Azure Functions as your webhook for the SharePoint List changes.

Create SharePoint Lists

  • Create a SharePoint list named Countries to maintain country values.
  • Create a SharePoint list named WebHookHistory to keep track of the changes done in Countries list with the following columns: AZF-SP-WH-11

Create Azure Function

  • Navigate to Azure Portal, select New, and then select Function App. AZF-SP-WH-01
  • On the Basics page, complete the information needed to create the Function App AZF-SP-WH-02
  • Select your hosting, monitoring settings, and then select Review + create to review the app configuration selections.
  • On the Review + create page, review your settings, and then select Create to provision and deploy the function app.
  • Select the Notification icon in the upper-right corner of the portal and watch for the Deployment succeeded message.
  • Select Go to resource to view your new function app.
  • After the function app is created, you can create functions in different languages.

Create an HTTP triggered function

  • Expand your new function app, select the + button next to Functions, choose HTTP Trigger AZF-SP-WH-03
  • On the New Function page, complete the information needed to create the new Function AZF-SP-WH-04

Configure Function Settings

  • Go to the Functions Apps blade and click on your Function. You will see an overview of the Function. Click on Platform features and click on Application settings
  • Update the configurations as shown below AZF-SP-WH-05
  • Switch the Allowed HTTP methods to Selected methods, and then only allow the POST HTTP method. Also cross-check that Mode is equal to Standard, and Authorization level is set to Anonymous. AZF-SP-WH-06

Include External DLL

  • Go to the Platform feature of the Functions App and click on Advanced tools (Kudu) to open the tool in another tab.
  • Click on Debug Console (CMD or PowerShell) and navigate to the wwwroot folder.
  • Create a new folder with name libraries and upload the necessary SharePoint DLLs in it AZF-SP-WH-07

Copy Code Snippet

Replace the default function code with the following code:

#r "Newtonsoft.Json"
#r "..\libraries\Microsoft.SharePoint.Client.dll"
#r "..\libraries\Microsoft.SharePoint.Client.Runtime.dll"

using Microsoft.Azure.WebJobs.Host;
using Microsoft.SharePoint.Client;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security;
using System.Threading.Tasks;

public static async Task<object> Run(HttpRequestMessage req, TraceWriter log)
{
log.Info(\$"Webhook triggered");

    string spTenantNameKey = "SPApp_TenantName";
    string spAdminUserNameKey = "SPApp_UserName";
    string spAdminPasswordKey = "SPApp_Password";
    string spListWebHookHistoryKey = "SPApp_List_WebHookHistory";

    string spTenantNameValue = System.Environment.GetEnvironmentVariable(spTenantNameKey, EnvironmentVariableTarget.Process);
    string spAdminUserNameValue = System.Environment.GetEnvironmentVariable(spAdminUserNameKey, EnvironmentVariableTarget.Process);
    string spAdminPasswordValue = System.Environment.GetEnvironmentVariable(spAdminPasswordKey, EnvironmentVariableTarget.Process);
    string spListWebHookHistoryValue = System.Environment.GetEnvironmentVariable(spListWebHookHistoryKey, EnvironmentVariableTarget.Process);

    log.Info($"Key values received");

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

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

    var content = await req.Content.ReadAsStringAsync();
    log.Info($"Received  payload: {content}");

    var notifications = JsonConvert.DeserializeObject<ResponseModel<NotificationModel>>(content).Value;
    log.Info($"Found {notifications.Count} notifications");

    if (notifications.Count > 0)
    {
        log.Info($"Processing notifications...");

        foreach (var notification in notifications)
        {
            string siteUrl = String.Format("https://{0}{1}", spTenantNameValue, notification.SiteUrl);

            using (var cc = GetClientContext(siteUrl, spAdminUserNameValue, spAdminPasswordValue))
            {
                ListCollection lists = cc.Web.Lists;

                Guid listId = new Guid(notification.Resource);
                IEnumerable<List> listsCountries = cc.LoadQuery(lists.Where(lst => lst.Id == listId));
                cc.ExecuteQuery();

                List changeList = listsCountries.FirstOrDefault();
                if (changeList == null)
                {
                    return new HttpResponseMessage(HttpStatusCode.OK);
                }

                IEnumerable<List> listsWebHookHistory = cc.LoadQuery(lists.Where(lst => lst.Title == spListWebHookHistoryValue));
                cc.ExecuteQuery();

                List historyList = listsWebHookHistory.FirstOrDefault();
                if (historyList == null)
                {
                    return new HttpResponseMessage(HttpStatusCode.OK);
                }

                ChangeQuery changeQuery = new ChangeQuery(false, true);
                changeQuery.Item = true;
                changeQuery.FetchLimit = 1000;

                bool allChangesRead = false;

                do
                {
                    ChangeToken lastChangeToken = new ChangeToken();
                    lastChangeToken.StringValue = string.Format("1;3;{0};{1};-1", notification.Resource, DateTime.Now.AddSeconds(-60).ToUniversalTime().Ticks.ToString());
                    changeQuery.ChangeTokenStart = lastChangeToken;
                    var changes = changeList.GetChanges(changeQuery);
                    cc.Load(changes);
                    cc.ExecuteQuery();

                    if (changes.Count > 0)
                    {
                        foreach (Change change in changes)
                        {
                            lastChangeToken = change.ChangeToken;

                            if (change is ChangeItem)
                            {
                                ListItemCreationInformation newItem = new ListItemCreationInformation();
                                ListItem item = historyList.AddItem(newItem);
                                item["Title"] = changeList.Title;
                                item["ChangeType"] = change.ChangeType.ToString();
                                item["ItemId"] = (change as ChangeItem).ItemId;
                                item.Update();
                                cc.ExecuteQuery();
                            }
                        }
                        if (changes.Count < changeQuery.FetchLimit)
                        {
                            allChangesRead = true;
                        }
                    }
                    else
                    {
                        allChangesRead = true;
                    }
                } while (allChangesRead == false);
            }
        }
    }

    return new HttpResponseMessage(HttpStatusCode.OK);

}

public static ClientContext GetClientContext(string SiteUrl, string userName, string password)
{
var credentials = new SharePointOnlineCredentials(userName, ToSecureString(password));
var context = new ClientContext(SiteUrl);
context.Credentials = credentials;

    return context;

}

public static SecureString ToSecureString(string Source)
{
if (string.IsNullOrWhiteSpace(Source))
return null;
else
{
SecureString Result = new SecureString();
foreach (char c in Source.ToCharArray())
Result.AppendChar(c);
return Result;
}
}

public class ResponseModel<T>
{
[JsonProperty(PropertyName = "value")]
public List<T> Value { get; set; }
}

public class NotificationModel
{
[JsonProperty(PropertyName = "subscriptionId")]
public string SubscriptionId { get; set; }

    [JsonProperty(PropertyName = "clientState")]
    public string ClientState { get; set; }

    [JsonProperty(PropertyName = "expirationDateTime")]
    public DateTime ExpirationDateTime { get; set; }

    [JsonProperty(PropertyName = "resource")]
    public string Resource { get; set; }

    [JsonProperty(PropertyName = "tenantId")]
    public string TenantId { get; set; }

    [JsonProperty(PropertyName = "siteUrl")]
    public string SiteUrl { get; set; }

    [JsonProperty(PropertyName = "webId")]
    public string WebId { get; set; }

}

public class SubscriptionModel
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Id { get; set; }

    [JsonProperty(PropertyName = "clientState", NullValueHandling = NullValueHandling.Ignore)]
    public string ClientState { get; set; }

    [JsonProperty(PropertyName = "expirationDateTime")]
    public DateTime ExpirationDateTime { get; set; }

    [JsonProperty(PropertyName = "notificationUrl")]
    public string NotificationUrl { get; set; }

    [JsonProperty(PropertyName = "resource", NullValueHandling = NullValueHandling.Ignore)]
    public string Resource { get; set; }

}

Create List Subscription

  • Refer this Microsoft article to understand the API Endpoints and permission requirements.
  • We will need to let SharePoint know what webhook URL we are using. To do so, let’s start by copying the Azure Function URL. AZF-SP-WH-08
  • To avoid unauthorized usage of your Azure Function, the caller needs to specify a code when calling your function. This code can be retrieved via the Manage screen. AZF-SP-WH-09
  • So, in our case the webhook URL to use is the following: azurefunctionurlvalue?code=functionkeyvalue
  • Let’s register the Function URL with the SharePoint List. In this example I am going to register my Azure Function with the list named Countries so that whenever a new item is added, the change details will be saved in WebHookHistory SharePoint list.
  • I used spo list webhook add CLI for Microsoft 365 command to register my azure function as webhook. Alternatively we can use SP Chrome Editor extension

Example: o365 spo list webhook add –webUrl yoursiteurl –listTitle Countries –notificationUrl azurefunctionurlvalue?code=functionkeyvalue

Demo

AZF-SP-WH-10

I hope you find this post helpful.