How to fetch meeting transcript using RSC based permission framework

Thomas Dehaene 30 Reputation points
2025-01-20T09:25:05.8833333+00:00

The goal

I want to build a Teams app, with an Azure bot (built using the Bot Framework Python SDK), added to a meeting.

When the meeting is started, a notification subscription is registered with the Graph API to alert an endpoint that the transcript is available.

The goal is to then automatically process that transcript (using some custom AI steps) to get insights from the meeting.

The problem

Almost all steps have gone well to develop, apart from the step to fetch the actual transcript text. There the Graph API is giving me troubles.

Steps undertaken

Please find a quick summary of the steps I have undertaken so far, hopefully this will give further insights.

App registration

An App registration has been made in our Entra ID tenant, providing a client-id and client-secret to provide to the Bot, and to later fetch a Graph API token.

No additional permissions have been granted to this app registration, since we want to purely use RSC based permissions, to have the smallest possible access scope.

A client-id and client-secret are noted and stored safely somewhere.

Azure bot backend

An Azure bot has been set up with a custom python backend, with the appropriate API endpoint as well to receive notification subscriptions under the /notify endpoint

Teams app manifest

A teams app manifest is defined to configure the bot access and list the necessary RSC permissions. You can find a redacted version here below:

{
    "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.19/MicrosoftTeams.schema.json",
    "manifestVersion": "1.19",
    "version": "1.0.9",
    "id": "<TEAMS-APP-EXTERNAL-ID>",
    "showLoadingIndicator": false,
    "developer": {
      "name": "<DEVNAME>",
      "websiteUrl": "https://www.microsoft.com",
      "privacyUrl": "https://www.microsoft.com/privacy.html",
      "termsOfUseUrl": "https://www.microsoft.com/termsofuse.html"
    },
    "name": {
      "short": "<SHORT-NAME>",
      "full": "<FULL-NAME>"
    },
    "description": {
      "short": "<SHORT-DESCRIPTION>",                                                             
      "full": "<FULL-DESCRIPTION>"
    },
    "icons": {
      "outline": "outline.png",
      "color": "color.png"
    },
    "accentColor": "#ffffff",
    "permissions": [
      "identity",
      "messageTeamMembers"
    ],
    "webApplicationInfo": {
      "id": "<AAD-CLIENT-ID>",
      "resource": "https://RscPermission"
    },
    "authorization": {
      "permissions": {
        "resourceSpecific": [
          {
            "name": "OnlineMeetingRecording.Read.Chat",
            "type": "Application"
          },
          {
            "name": "OnlineMeetingTranscript.Read.Chat",
            "type": "Application"
          },
          {
            "name": "OnlineMeeting.ReadBasic.Chat",
            "type": "Application"
          },
          {
            "name": "ChannelMeeting.ReadBasic.Group",
            "type": "Application"
          },
          {
            "name": "OnlineMeetingParticipant.Read.Chat",
            "type": "Application"
          },
          {
            "name": "ChannelMessage.Read.Group",
            "type": "Application"
          },
          {
            "name": "ChatMessage.Read.Chat",
            "type": "Application"
          }
        ]
      }
    },
    "validDomains": [
        "<BOT-BACKEND-URL>",
        "token.botframework.com"
      ],
    "bots": [
      {
        "botId": "<AAD-CLIENT-ID>",
        "scopes": [
          "personal",
          "team",
          "groupChat"
        ],
        "needsChannelSelector": false,
        "isNotificationOnly": false,
        "supportsFiles": false
      }
    ]
  }

Publish and approve app

The app as above has been submitted for approval, has been approved and is visible under the "Built for your org" section of custom apps in the Teams desktop client.

Register notification subscription on meeting start

The bot gets a notification under the on_teams_meeting_start_event that the meeting has started. A piece of code is then executed to register a notification subscription on the Graph API, so that the backend receives a REST call when the transcript is available. This is done as follows:

subscription_url = "https://graph.microsoft.com/beta/subscriptions"
app_id=<TEAMS-APP-INTERNAL-ID>
base64_certificate=<CERTIFICATE-PUBLIC-KEY>
certificate=<CERTIFICATE-ID>
headers = headers = {
    "Authorization": f"Bearer <TOKEN FETCHED VIA AAD APP REG CLIENT ID AND SECRET>",
}

subscription_data = {
    "changeType": "created",
    "notificationUrl": "<BACKEND-URL>/api/notify",
    "resource": f"appCatalogs/teamsApps/{app_id}/installedToOnlineMeetings/getAllTranscripts?useResourceSpecificConsentBasedAuthorization=true",
    "includeResourceData": True,
    "expirationDateTime": "<EXPIRATION-DATETIME>",
	"clientState": "<STATE-STRING>",
    "encryptionCertificate": base64_certificate,
    "encryptionCertificateId": cert_id,
}

response = requests.post(subscription_url, headers=headers, json=subscription_data)
print(response.status)
print(response.json())

This correctly results in a 201 status with following body:

{
  "@odata.context": "https://graph.microsoft.com/beta/$metadata#subscriptions/$entity",
  "id": "<SUBSCRIPTION-ID>", 
  "resource": "appCatalogs/teamsApps/<TEAMS-APP-INTERNAL-ID>/installedToOnlineMeetings/getAllTranscripts?useResourceSpecificConsentBasedAuthorization=true", 
  "applicationId": "<AAD-CLIENT-ID>", 
  "changeType": "created",
  "clientState": "<STATE-STRING>",
  "notificationUrl": "<NOTIFICATION-URL-ENDPOINT>",
  "notificationQueryOptions": null,
  "notificationContentType": null,
  "lifecycleNotificationUrl": null,
  "expirationDateTime": "<EXPIRATION-DATETIME>",
  "creatorId": "CREATOR-ID",
  "includeResourceData": true,
  "latestSupportedTlsVersion": "v1_2", 
  "encryptionCertificate": "<CERTIFICATE-PUBLIC-KEY>", 
  "encryptionCertificateId": "<CERTIFICATE-ID>",
  "notificationUrlAppId": null
}

Capture transcript available event

When the meeting is done, I correctly get a notification event, as described int the online documentation.

For example:

{
  "tenantId": "<TENANT-ID>",
  "changeType": "created",
  "subscriptionId": "<SUBSCRIPTION-ID>",
  "clientState": "<STATE-STRING>",
  "subscriptionExpirationDateTime": "<EXPIRATION-DATETIME>",
  "resource": "communications/onlineMeetings('<MEETING-ID>')/transcripts('<TRANSCRIPT-ID>')",
  "resourceData": {
    "id": "<TRANSCRIPT-ID>",
    "@odata.type": "#Microsoft.Graph.callTranscript",
    "@odata.id": "communications/onlineMeetings('<MEETING-ID>')/transcripts('TRANSCRIPT-ID')",
  },
  "encryptedContent": {
    "data": "<ENCRYPTED-CONTENT>",
    "dataSignature": "<DATA-SIGNATURE>",
    "dataKey": "<DATA-KEY>",
    "encryptionCertificateId": "<CERTIFICATE-ID>",
    "encryptionCertificateThumbprint": "<CERTIFICATE-THUMBRPINT>",
  },
}

I was kind of hoping the encrypted content would already contain the transcript, but upon decryption it does not:

{'@odata.context': "https://graph.microsoft.com/$metadata#communications/onlineMeetings('<MEETING-ID>')/transcripts/$entity",
 'id': '<TRANSCRIPT-ID>',
 'meetingId': '<MEETING-ID>',
 'callId': '<CALL-ID>',
 'contentCorrelationId': '<CONTENT-CORR-ID>',
 'createdDateTime': '...',
 'endDateTime': '...',
 'transcriptContentUrl': 'communications/onlineMeetings/<MEETING-ID>/transcripts/<TRANSCRIPT-ID>/content',
 'meetingOrganizer': {'application': None,
  'device': None,
  'user': {'userIdentityType': 'aadUser',
   'tenantId': '<TENANT-ID>',
   'id': '...',
   'displayName': None}},
 'content': None}

Fetch the transcript content

This is the step where it goes wrong.

Using a GET request to this url

https://graph.microsoft.com/v1.0/<TRANSCRIPT-CONTENT-URL-FROM-ABOVE>

Using the same Bearer token as earlier (so obtained via the AAD app client-id and client-secret) results in a 404 response:

{
  "error": {
    "code":"UnknownError",
    "message":"404 page not found\\n",
    "innerError": {
      "date":"2025-01-20T09:20:12",
      "request-id":"c78ce659-fa44-4a98-bd48-a82ac0447c5f",
      "client-request-id": "c78ce659-fa44-4a98-bd48-a82ac0447c5f"
    }
  }
}

So I am lost as why the content url, that was provided by the notification subscription event, could result in a 404.

Which step am I missing?

I have searched all online docs and threads for answers, but unfortunately cannot find any at the moment.

All help is greatly appreciated.

Microsoft Graph
Microsoft Graph
A Microsoft programmability model that exposes REST APIs and client libraries to access data on Microsoft 365 services.
13,310 questions
Microsoft Teams Development
Microsoft Teams Development
Microsoft Teams: A Microsoft customizable chat-based workspace.Development: The process of researching, productizing, and refining new or existing technologies.
3,692 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Saranya Madhu-MSFT 1,975 Reputation points Microsoft External Staff
    2025-01-23T08:32:10.3766667+00:00

    Hi Thomas Dehaene,

    Thanks for reaching out to Microsoft!

    As per the Microsoft documentation, to get the content of a single transcript of an online meeting.

    GET me/onlineMeetings/{meetingId}/transcripts/{transcriptId}/content
    GET users/{userId}/onlineMeetings/{meetingId}/transcripts/{transcriptId}/content
    

    Response contains bytes for the transcript in the body. content-type header specifies type of the transcript content.

    Get a callTranscript content specifying $format query param:

    GET https://graph.microsoft.com/v1.0/users/ba321e0d-79ee-478d-8e28-85a19507f456/onlineMeetings/{onlinemeetingID}/transcripts/{transcriptID}/content?$format=text/vtt

    Hope this helps.

    If the answer is helpful, please click Accept Answer and kindly upvote. If you have any further questions about this answer, please click Comment.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.