How to: Use the new Webhooks for Microsoft Teams Channel & Chat Messages
We’ve known for a little while that Microsoft have been looking at adding webhook support for Microsoft Teams data. Thanks to a session by Bill Bliss at Microsoft Ignite 2019 and because things are now rolling out and lighting up, we now know what that’s going to look like, and what webhooks will be available.
In fact, even though the documentation isn’t quite caught up yet, the webhooks described below work today and you can try them out. Read on for a fully worked example of how to set them up.
These webhooks are in Beta, so shouldn’t be used for production environments. Also, they are VERY new and not all the documentation is live yet. Bear all that in mind when reading and acting on this blog post. 🙂
What is a webhook anyway?
I’m going to assume you’re already aware of what Microsoft Graph is, and that you have some experience already in using it to get Microsoft Teams data.
What you’ve no doubt noticed is that using Graph today you can get lots of information about teams, channels and conversations, but it’s very much a “point in time” request for data. If you’re trying to track something happening, or respond to events, the only approach you have really is to continually poll for changes, comparing the data with what you have.
Webhooks provide a way for the end system (Teams/Graph) to actively tell you when something changes, a bit like a notification on your mobile phone. You can then ignore it, or react to it.
Webhooks work by you choosing to subscribe to a specific set of changes and providing a URL. When that change happens, Graph will then send you a payload of data describing the change to the URL you specify. The URL is usually an API endpoint which is capable of doing something with that payload of data.
More information about Graph Webhooks, how they work and how you set them up can be found on the Webhooks Overview page.
What webhooks are available today for Microsoft Teams?
There are two webhooks we know about for sure: per-channel webhooks, and per-chat webhooks.
Per-Channel Webhooks
These were mentioned by Bill in BRK3226. Specifically, you’ll be able to subscribe to a specific channel in a Team and then receive notifications about “things” that happen in that channel. “Things” include: new messages, edited messages, deleted messages, new replies to existing messages, any reactions to messages (such as thumbs up).
Per-Chat Webhooks
There is also webhook which can be used to track new 1:1 and group chat messages. This wasn’t mentioned in the session, but it’s listed in a PR for the docs with the following description:Â Listen for new and edited chat messages, and reactions to them.
How do I use them?
Firstly, it’s worth understanding in general how webhooks work in Graph and maybe even try them out using an existing workload in Graph v1. Today you can use webhooks to subscribe to changes to users, files, events etc. To get started, visit the Create Subscription page. In this post, I’m going to assume you’ve used webhooks before and just concentrate on the specifics.
All subscription requests and updates go to: https://graph.microsoft.com/beta/subscriptions as a POST request, with a body that defines what you want to subscribe to:
{ "changeType": "created", "notificationUrl": "https://msteamswebhooks.azurewebsites.net/api/HttpTrigger1", "resource": "RESOURCE_TO_SUBSCRIBE_TO", "expirationDateTime":"2020-01-02T12:00:00.0Z", "clientState": "secretClientValue" }
The changeType value can be: created, updated or deleted. Multiple values can be combined using a comma-separated list.
The notificationUrl is the URL that you want Graph to notify you on when the change happens. This endpoint should be OK with receiving POST requests with JSON body content.
The resource value is the “thing” you want to subscribe to. The new resources (as described above) are:
/teams/{id}/channels/{id}/messages – to subscribe to messages in a specific channel
/chats/{id}/messages – to subscribe to messages in a specific chat thread
When you make this request, you’ll need to supply a bearer token for an application that has the appropriate permissions to see this data - ChannelMessage.Read.All or Chat.Read.All respectively.
If everything went OK, the response will be 201 and will confirm the details of your subscription, and the subscription expiration time (more on this in a moment).
Example: Subscribing to channel updates
In the screenshot below, I’m setting up a new subscription for messages in a specific channel, the ThoughtStuff General channel (the body is same as the code above). You can see that I’m sending notifications to an Azure Functions endpoint, and you can see the response from the call confirming the subscription:
In the Azure Function code that sits behind the URL, I’m simply logging the JSON body that I receive. (Also note the first few lines: when creating a subscription you need to respond to a validation request, the standard Graph docs for webhooks has all the detail about this). Feel free to use this as a good starting point for your own webhook code:
#r "Newtonsoft.Json" using System.Net; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; public static async Task<IActionResult> Run(HttpRequest req, ILogger log) { log.LogInformation("C# HTTP trigger function processed a request."); //when creating a subscription, graph will send a validation token and expect it to come back, to test the endpoint string validationToken = req.Query["validationToken"]; if (validationToken != null) return (ActionResult)new OkObjectResult(validationToken); string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); log.LogInformation("Everything:" + requestBody); return new OkObjectResult("ok"); }
When I create a new conversation in that channel, the webhook is triggered and my API is called. This is the output from that Azure Function (which I collected via the Live Metrics Stream):
{ "value":[ { "subscriptionId":"7752cead-a48f-4d8e-8354-6e29da867703", "changeType":"created", "tenantId":"6c81c6cd-2fa1-4985-a0c7-6dadb192bc8b", "clientState":"secretClientValue", "subscriptionExpirationDateTime":"2020-01-02T15:32:00+00:00", "resource":"teams('b01339a1-ca3f-455e-bc47-5c92fec169f1')/channels('19:[email protected]')/messages('1577975699457')", "resourceData":{ "@odata.type":"#Microsoft.Graph.ChatMessage", "@odata.id":"teams('b01339a1-ca3f-455e-bc47-5c92fec169f1')/channels('19:[email protected]')/messages('1577975699457')" }, "encryptedContent":null } ] }
It’s a really similar process for the chat subscription as well. (so I haven’t shown it)
Subscription Expiration Lengths
If you’ve used other Graph webhooks, then you might be expecting to be able to set a subscription expiration date anytime up to 3 day into the future, because this is the Graph default. However, if you try and do that, you’ll get this error message returned when you attempt to create the subscription:
{ "error": { "code": "ExtensionError", "message": "Subscription expiration can only be 60 minutes in the future.", "innerError": { "request-id": "da63ad13-b5db-41aa-b3e1-dd3c95f661ab", "date": "2020-01-02T14:31:55" } } }
That’s because you can only set a subscription up to 60 minutes. The reason given for this (which I’m not 100% sure I understand) is to do with the ability to return actual data in the payload (more on this in the next section). For security reasons, shorter subscription times have been used, causing you to re-subscribe (or renew the subscription) with a valid bearer token more often.
So, what’s actually returned?
Let’s look at that return object again, that gets returned for a new channel message:
{ "value":[ { "subscriptionId":"7752cead-a48f-4d8e-8354-6e29da867703", "changeType":"created", "tenantId":"6c81c6cd-2fa1-4985-a0c7-6dadb192bc8b", "clientState":"secretClientValue", "subscriptionExpirationDateTime":"2020-01-02T15:32:00+00:00", "resource":"teams('b01339a1-ca3f-455e-bc47-5c92fec169f1')/channels('19:[email protected]')/messages('1577975699457')", "resourceData":{ "@odata.type":"#Microsoft.Graph.ChatMessage", "@odata.id":"teams('b01339a1-ca3f-455e-bc47-5c92fec169f1')/channels('19:[email protected]')/messages('1577975699457')" }, "encryptedContent":null } ] }
You’ll notice that there isn’t anything there that tells me what the message actually was. However, there is enough information about the message to enable you to go and look it up for yourself (in this case by making a GET to https://graph.microsoft.com/beta/teams/b01339a1-ca3f-455e-bc47-5c92fec169f1/channels/19:[email protected]/messages/1577975699457).
Optionally, encrypt calls for full content
For some workloads, that’s absolutely fine. However, there is another way. You can actually request that the full information about the message is sent to you in the notification. It’s a bit trickier to set up though, because Graph will be including sensitive data including message text to an endpoint that’s outside of Graph.
To achieve it, you’ll need to have a certificate which you’ve exported as a Base64-encoded X.509 format, which you’ll then pass to Graph when you create the subscription. Graph will then encrypt the contents using the public key, which you can then decrypt with the private key. For more information on setting this up, see Set up change notifications that include resource data.
To tell Graph that’s what you want to do, you modify the subscription request body to include 3 extra values – includeResourceData, encryptionCertificate, and encryptionCertificateID:
{ "changeType": "created", "notificationUrl": "https://msteamswebhooks.azurewebsites.net/api/HttpTrigger1", "resource": "/teams/b01339a1-ca3f-455e-bc47-5c92fec169f1/channels/19:[email protected]/messages", "expirationDateTime":"2020-01-02T15:32:00.0Z", "clientState": "secretClientValue", "includeResourceData": "true", "encryptionCertificate": "BASE_64_ENCODED_CERTIFICATE", "encryptionCertificateId": "something secret" }
encryptionCertificateID can be anything you like, Graph will include it in the encrypted response so you can verify that it actually came from Graph.
(note: as I publish this blog post, although I can successfully create a subscription with includeResourceData set to true, it doesn’t seem to ever notify my notificationUrl. I’m still looking into this to see whether it’s something I’m doing wrong, or a known issue. Updates when I have them.)
If you plan to do this, then check out some code samples of how to decrypt the data that’s returned.
>>Thatâs because you can only set a subscription up to 60 minutes. The reason given for this (which Iâm not 100% sure I understand) is to do with the ability to return actual data in the payload (more on this in the next section).
Microsoft may have other reasons, but this subscription could be quite resource intensive for their infra based on the message rate in the channel/chat. A shorter subscription ensures the webhook end point is still around to receive the call backs, and by renewing it the so it also acts as a keep alive. (If the webhook end point was disconnected and the subscription was set to 3 days, the sender would still have to attempt to call the end point on every message etc. I suppose they could shutdown the subscription on multiple errors but asking for a renewal has the same effect.)
Hi,
I am trying to subscribe to a Team’s Channel Messages. I am using a token from an app that has the following scopes.
“scope”: “ChannelMessage.Read.All Chat.Read Chat.ReadWrite Group.Read.All Group.ReadWrite.All User.Read profile openid email”
However, whenever i try to subscribe i get the error below
“code”: “ExtensionError”,
“message”: “Operation: Create; Exception: [Status Code: Unauthorized; Reason: Caller does not have access to ‘TEAMS/{teamid}/CHANNELS/{channelid}/MESSAGES’ resource]”,
If i try to run call a get teams/{teamid}/channels/{channelid}/messages endpoint using the same token, I am able to get the messages, so I am not sure why I do not have access.
Any ideas?
Thanks
good article!
when i post a GET like you have above i keep getting “code”: “UnknownError”
cant figure out what i am doing wrong. ugh