Webhook notification when Microsoft Teams presence changes using Graph API

Overview
 
There are multiple business scenarios when in an application you would need to display up to date presence information of a user from Microsoft Teams.
Microsoft had announced getPresence endpoint to get the user's presence, including their availability and user activity. However, this is a pull mechanism, your application had to make continous polling "get" call to know the current status and if the status is changed in the interim there was no way to know the latest status.
To address this issue, Microsoft has announced a presence subscription API, this API can be used to subscribe other users presence in Microsoft Teams. 
Once you create a subscription for the presence for a user and register a HTTPS endpoint to receive a notification, Microsoft Graph will tell you when there are changes. Typically this subscription is good for up to 60 minutes but you can renew them using the "update subscription" endpoint.
 
 
In this article, we will see the step by step configuration to get the users presence status from Microsoft Teams automatically.
 
Prerequisites 
 
  • App registered in Azure active directory(AAD) with Presence.Read and Presence.Read all delegated permissions. 
  • Install ngrokTool (For Development)
  • HTTPS endpoint up and running (For PROD)
 
Create a web API to receive notifications : 
 
Microsoft Graph will send notifications to a registered HTTPS endpoint. In this case, we will create a ASP.NET Core web API, to receive the notifications.
ngrok is a free tool for sharing projects online that are created in localhost. During development, you can use the ngrok tool to tunnel the requests via the internet. 
 
1. Open the command prompt and navigate to the directory where you would like to create the project
2. Execute below commands to create ASP.net core Web API
 
  1.   dotnet new webapi -o PresenceReceiverAPI
 
 3. Once the project is created, navigate to the project directory and execute below commands in the command prompt:
 Pl note we are using Microsoft.Graph.Beta instead of Microsoft.Graph so that we can call the beta endpoint of Microsoft Graph API.
  1. cd PresenceReceiverAPI  
  2. dotnet add package Microsoft.Identity.Client  
  3. dotnet add package Microsoft.Graph.Beta --version 0.20.0-preview  
  4. dotnet run  
4.Once you excute above commands application will start and display following in the console: 
 
  1. info: Microsoft.Hosting.Lifetime[0]  
  2.       Now listening on: https://localhost:5001  
  3. info: Microsoft.Hosting.Lifetime[0]  
  4.       Now listening on: http://localhost:5000  
  5. info: Microsoft.Hosting.Lifetime[0]  
  6.       Application started. Press Ctrl+C to shut down.  
  7. info: Microsoft.Hosting.Lifetime[0]  
  8.       Hosting environment: Development  
  9. info: Microsoft.Hosting.Lifetime[0]  
  10.       Content root path: [your file path]\PresenceReceiverAPI  
 5. Press Ctrl+C to stop the application and open your favorite IDE (I like Visual Studio Code.. )  to add controllers that receive the notifications.
 6. Add below function in the controller class to receive the notification. 
  1. [HttpGet]  
  2.        public async Task<ActionResult<string>> Get()  
  3.        {  
  4.        var graphServiceClient = GetGraphClient();  
  5.        var sub = new Microsoft.Graph.Subscription();  
  6.        sub.ChangeType = "updated";  
  7.        sub.NotificationUrl = config.Ngrok + "/api/notifications";  
  8.        sub.Resource = "/users";  
  9.        sub.ExpirationDateTime = DateTime.UtcNow.AddMinutes(5);  
  10.        sub.ClientState = "SecretClientState";  
  11.        var newSubscription = await graphServiceClient  
  12.            .Subscriptions  
  13.            .Request()  
  14.            .AddAsync(sub);  
  15.   
  16.        Subscriptions[newSubscription.Id] = newSubscription;  
  17.   
  18.        if (subscriptionTimer == null)  
  19.        {  
  20.            subscriptionTimer = new Timer(CheckSubscriptions, null, 5000, 15000);  
  21.        }  
  22.   
  23.        return $"Subscribed. Id: {newSubscription.Id}, Expiration: {newSubscription.ExpirationDateTime}";  
  24.        }  
 7. Add below function in the controller class,  to verify the validationToken and process the received notification.
  1.   public async Task<ActionResult<string>> Post([FromQuery]string validationToken = null)  
  2. {  
  3. // verify the validation token  
  4. if (!string.IsNullOrEmpty(validationToken))  
  5. {  
  6.   Console.WriteLine($"Received Token: '{validationToken}'");  
  7.   return Ok(validationToken);  
  8. }  
  9.   
  10. // process the notifications and display on the console  
  11. using (StreamReader reader = new StreamReader(Request.Body))  
  12. {  
  13.   string content = await reader.ReadToEndAsync();  
  14.   
  15.   Console.WriteLine(content);  
  16.   
  17.   var notifications = JsonConvert.DeserializeObject<Notifications>(content);  
  18.   
  19.   foreach (var notification in notifications.Items)  
  20.   {  
  21.     Console.WriteLine($"Received notification: '{notification.Resource}', {notification.ResourceData?.Id}");  
  22.   }  
  23. }  
  24.   
  25. await CheckForUpdates();  
  26.   
  27. return Ok();  
  28. }
 8. Once you add the code to fetch the access token, "controller" code should be as below:
 
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.IO;  
  4. using System.Linq;  
  5. using System.Net.Http;  
  6. using System.Threading.Tasks;  
  7. using Microsoft.AspNetCore.Mvc;  
  8. using msgraphapp.Models;  
  9. using Newtonsoft.Json;  
  10. using System.Net;  
  11. using System.Threading;  
  12. using Microsoft.Graph;  
  13. using Microsoft.Identity.Client;  
  14. using System.Net.Http.Headers;  
  15.   
  16. namespace PresenceReceiverAPI.Controllers  
  17. {  
  18.   [Route("api/[controller]")]  
  19.   [ApiController]  
  20.   public class NotificationsController : ControllerBase  
  21.   {  
  22.     private readonly MyConfig config;  
  23.   
  24.     public NotificationsController(MyConfig config)  
  25.     {  
  26.       this.config = config;  
  27.     }  
  28.   
  29.     [HttpGet]  
  30.     [HttpGet]  
  31.         public async Task<ActionResult<string>> Get()  
  32.         {  
  33.         var graphServiceClient = GetGraphClient();  
  34.         var sub = new Microsoft.Graph.Subscription();  
  35.         sub.ChangeType = "Updated";  
  36.         sub.NotificationUrl = config.Ngrok + "/api/notifications";  
  37.         sub.Resource = "/communications/presences/" + config.User;  
  38.         sub.ExpirationDateTime = DateTime.UtcNow.AddMinutes(5);  
  39.         sub.ClientState = "SecretClientState";  
  40.         var newSubscription = await graphServiceClient  
  41.             .Subscriptions  
  42.             .Request()  
  43.             .AddAsync(sub);  
  44.   
  45.         Subscriptions[newSubscription.Id] = newSubscription;  
  46.   
  47.         if (subscriptionTimer == null)  
  48.         {  
  49.             subscriptionTimer = new Timer(CheckSubscriptions, null, 5000, 15000);  
  50.         }  
  51.   
  52.         return $"Id of the Subscription: {newSubscription.Id}, Subscription Expires on: {newSubscription.ExpirationDateTime}";  
  53.         }  
  54.   
  55.     public async Task<ActionResult<string>> Post([FromQuery]string validationToken = null)  
  56.     {  
  57.       // handle validation  
  58.       if(!string.IsNullOrEmpty(validationToken))  
  59.       {  
  60.         Console.WriteLine($"Received Token: '{validationToken}'");  
  61.         return Ok(validationToken);  
  62.       }  
  63.   
  64.       // handle notifications  
  65.       using (StreamReader reader = new StreamReader(Request.Body))  
  66.       {  
  67.         string content = await reader.ReadToEndAsync();  
  68.   
  69.         Console.WriteLine(content);  
  70.   
  71.         var notifications = JsonConvert.DeserializeObject<Notifications>(content);  
  72.   
  73.         foreach(var notification in notifications.Items)  
  74.         {  
  75.           Console.WriteLine($"Received notification: '{notification.Resource}', {notification.ResourceData?.Id}");  
  76.         }  
  77.       }  
  78.   
  79.       return Ok();  
  80.     }  
  81.   
  82.     private GraphServiceClient GetGraphClient()  
  83.     {  
  84.       var graphClient = new GraphServiceClient(new DelegateAuthenticationProvider((requestMessage) => {  
  85.         // get an access token for Graph  
  86.         var accessToken = GetAccessToken().Result;  
  87.   
  88.         requestMessage  
  89.             .Headers  
  90.             .Authorization = new AuthenticationHeaderValue("bearer", accessToken);  
  91.   
  92.         return Task.FromResult(0);  
  93.       }));  
  94.   
  95.       return graphClient;  
  96.     }  
  97.   
  98.     private async Task<string> GetAccessToken()  
  99.     {  
  100.       IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(config.AppId)  
  101.         .WithClientSecret(config.AppSecret)  
  102.         .WithAuthority($"https://login.microsoftonline.com/{config.TenantId}")  
  103.         .WithRedirectUri("https://daemon")  
  104.         .Build();  
  105.   
  106.       string[] scopes = new string[] { "https://graph.microsoft.com/.default" };  
  107.   
  108.       var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();  
  109.   
  110.       return result.AccessToken;  
  111.     }  
  112.   
  113.   }  
  114. }  
 
 
 
Create Subscription
 
1)  Create a change notification subscription by submitting an HTTP POST request to the subscriptions endpoint: 
  1. https://graph.microsoft.com/beta/subscriptions  
Let's look at the parameters that are required :
  •  changeType - when should the notification trigge. Presence subscription API only support "Updated" parameter.
  •  notificationUrl - registered HTTPS Endpoint which is up and running. If the endpoint is down, subscription is not created.
  •  resource  - Presence parameter. There are two options to get the presence status : For single-user presence - /communications/presences/{id} For multiple user presence /communications/presences?$filter=id in ({id},{id}...)
  • expirationDateTime - subscription expiration time no more than 60 minutes from the time subscription is created.
 
Typical Post request will appear as below : 
  1. {  
  2.   "changeType""Updated",  
  3.   "clientState""SecretClientState",  
  4.   "notificationUrl""https://f44426ca147b.ngrok.io/api/notifications",  
  5.   "resource""/communications/presences/9974a8dc-d808-421e-8f31-76fbf74f7f1f",  
  6.   "expirationDateTime""2020-08-08T04:30:28.2257768+00:00"  
  7. }  
 
Test the application:
 
1. Within Visual Studio Code, select Run > Start debugging to run the application.
2. Navigate to the following url: http://<URL created in ngrok>/api/notifications. This will register a new subscription.
3. Now change the status of the user for which subscription is created in Microsoft teams
4. As soon as you change the status, a notification will be triggered and visible in your console.
Below is a typical notification response your HTTPS endpoint will receive, once the status of a user is changed.
 Two key properties are returned by API : 
  • availability, or base presence information e.g: Available, Away, Busy
  • activity, or information that’s supplemental to the availability  e.g: InAMeeting, InACall
  1. {  
  2.     "value": [  
  3.         {  
  4.             "subscriptionId""1221b5ed-ec3f-4036-ac38-042838d55a6f",  
  5.             "clientState""SecretClientState",  
  6.             "changeType""updated",  
  7.             "resource""communications/presences({id})",  
  8.             "subscriptionExpirationDateTime""2020-08-07T21:39:48.2162893+00:00",  
  9.             "resourceData": {  
  10.                 "@odata.type""#Microsoft.Graph.presence",  
  11.                 "@odata.id""communications/presences({id})",  
  12.                 "id"null,  
  13.                 "activity""Busy",  
  14.                 "availability""Busy"  
  15.             },  
  16.             "tenantId""eafd6ad4-0e37-4c74-b8b4-93a6118d9e75"  
  17.         }  
  18.     ]  
  19. }  
 
 
 
 
 
 
Note :
  • Presence subscription API is currently in public preview in Microsoft Graph beta.
  • It only supports Delegated user permissions, which means deamon application cannot call this API (as of now).
  • Microsoft.Graph NuGet package points to the Microsoft Graph v1.0 version, so you cannot create a subscription using it.