Message Extension Settings
```
Save it in the `index.html` file in the same folder as where your app is initialized.
You can serve it by adding the following code to your app:
```python
app.page("settings", str(Path(__file__).parent), "/tabs/settings")
```
:::note
This will serve the HTML page to the `$BOT_ENDPOINT/tabs/settings` endpoint as a tab.
:::
## 3. Specify the URL to the settings page
To enable the settings page, your app needs to handle the `message.ext.query-settings-url` activity that Teams sends when a user right-clicks the app in the compose box. Your app must respond with the URL to your settings page. Here's how to implement this:
```python
@app.on_message_ext_query_settings_url
async def handle_message_ext_query_settings_url(ctx: ActivityContext[MessageExtensionQuerySettingUrlInvokeActivity]):
user_settings = {"selectedOption": ""}
escaped_selected_option = user_settings["selectedOption"]
bot_endpoint = os.environ.get("BOT_ENDPOINT", "")
settings_action = CardAction(
type=CardActionType.OPEN_URL,
title="Settings",
value=f"{bot_endpoint}/tabs/settings?selectedOption={escaped_selected_option}",
)
suggested_actions = MessagingExtensionSuggestedAction(actions=[settings_action])
result = MessagingExtensionResult(type=MessagingExtensionResultType.CONFIG, suggested_actions=suggested_actions)
return MessagingExtensionInvokeResponse(compose_extension=result)
```
## 4. Handle Form Submission
When a user submits the settings form, Teams sends a `message.ext.setting` activity with the selected option in the `activity.value.state` property. Handle it to save the user's selection:
```python
@app.on_message_ext_setting
async def handle_message_ext_setting(ctx: ActivityContext[MessageExtensionSettingInvokeActivity]):
state = getattr(ctx.activity.value, "state", None)
if state == "CancelledByUser":
result = MessagingExtensionResult(
type=MessagingExtensionResultType.RESULT, attachment_layout=AttachmentLayout.LIST, attachments=[]
)
return MessagingExtensionInvokeResponse(compose_extension=result)
selected_option = state
await ctx.send(f"Selected option: {selected_option}")
result = MessagingExtensionResult(
type=MessagingExtensionResultType.RESULT, attachment_layout=AttachmentLayout.LIST, attachments=[]
)
return MessagingExtensionInvokeResponse(compose_extension=result)
```
---
### 📖 Message Extensions
# 📖 Message Extensions
Message extensions (or Compose Extensions) allow your application to hook into messages that users can send or perform actions on messages that users have already sent. They enhance user productivity by providing quick access to information and actions directly within the Teams interface. Users can search or initiate actions from the compose message area, the command box, or directly from a message, with the results returned as richly formatted cards that make information more accessible and actionable.
There are two types of message extensions: [API-based](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/api-based-overview) and [Bot-based](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/build-bot-based-message-extension?tabs=search-commands). API-based message extensions use an OpenAPI specification that Teams directly queries, requiring no additional application to build or maintain, but offering less customization. Bot-based message extensions require building an application to handle queries, providing more flexibility and customization options. This SDK supports bot-based message extensions only.
## Resources
- [What are message extensions?](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/what-are-messaging-extensions?tabs=desktop)
---
### Handling Multi-Step Forms
# Handling Multi-Step Forms
Dialogs can become complex yet powerful with multi-step forms. These forms can alter the flow of the survey depending on the user's input or customize subsequent steps based on previous answers.
Start off by sending an initial card in the `dialog_open` event.
```python
dialog_card = AdaptiveCard.model_validate(
{
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "text": "This is a multi-step form", "size": "Large", "weight": "Bolder"},
{
"type": "Input.Text",
"id": "name",
"label": "Name",
"placeholder": "Enter your name",
"isRequired": True,
},
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit",
"data": {"submissiondialogtype": "webpage_dialog_step_1"},
}
],
}
)
```
Then in the submission handler, you can choose to `continue` the dialog with a different card.
```python
@app.on_dialog_submit
async def handle_dialog_submit(ctx: ActivityContext[TaskSubmitInvokeActivity]):
"""Handle dialog submit events for all dialog types."""
data: Optional[Any] = ctx.activity.value.data
dialog_type = data.get("submissiondialogtype") if data else None
if dialog_type == "webpage_dialog":
name = data.get("name") if data else None
email = data.get("email") if data else None
await ctx.send(f"Hi {name}, thanks for submitting the form! We got that your email is {email}")
return InvokeResponse(
body=TaskModuleResponse(task=TaskModuleMessageResponse(value="Form submitted successfully"))
)
elif dialog_type == "webpage_dialog_step_1":
name = data.get("name") if data else None
next_step_card = AdaptiveCard.model_validate(
{
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "text": "Email", "size": "Large", "weight": "Bolder"},
{
"type": "Input.Text",
"id": "email",
"label": "Email",
"placeholder": "Enter your email",
"isRequired": True,
},
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit",
"data": {"submissiondialogtype": "webpage_dialog_step_2", "name": name},
}
],
}
)
return InvokeResponse(
body=TaskModuleResponse(
task=TaskModuleContinueResponse(
value=CardTaskModuleTaskInfo(
title=f"Thanks {name} - Get Email",
card=card_attachment(AdaptiveCardAttachment(content=next_step_card)),
)
)
)
)
elif dialog_type == "webpage_dialog_step_2":
name = data.get("name") if data else None
email = data.get("email") if data else None
await ctx.send(f"Hi {name}, thanks for submitting the form! We got that your email is {email}")
return InvokeResponse(
body=TaskModuleResponse(task=TaskModuleMessageResponse(value="Multi-step form completed successfully"))
)
return TaskModuleResponse(task=TaskModuleMessageResponse(value="Unknown submission type"))
```
---
### In-Depth Guides
# In-Depth Guides
This documentation covers advanced features and capabilities of the Teams SDK in Python.
This section provides comprehensive technical guides for integration with useful Teams features. Learn how to implement AI-powered bots, create adaptive cards, manage authentication flows, and build sophisticated message extensions. Each guide includes practical examples and best practices for production applications.
---
### 🔒 User Authentication
# 🔒 User Authentication
At times agents must access secured online resources on behalf of the user, such as checking email, checking on flight status, or placing an order. To enable this, the user must authenticate their identity and grant consent for the application to access these resources. This process results in the application receiving a token, which the application can then use to access the permitted resources on the user's behalf.
:::info
This is an advanced guide. It is highly recommended that you are familiar with [Teams Core Concepts](/teams/core-concepts) before attempting this guide.
:::
:::warning
User authentication does not work with the developer tools setup. You have to run the app in Teams. Follow these [instructions](/typescript/getting-started/running-in-teams#debugging-in-teams) to run your app in Teams.
:::
:::info
It is possible to authenticate the user into [other auth providers](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-identity-providers?view=azure-bot-service-4.0&tabs=adv2%2Cga2#other-identity-providers) like Facebook, Github, Google, Dropbox, and so on.
:::
Once you have configured your Azure Bot resource OAuth settings, as described in the [official documentation](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0), add the following code to your `App`:
## Project Setup
### Create an app with the `graph` template
:::tip
Skip this step if you want to add the auth configurations to an existing app.
:::
Use your terminal to run the following command:
```sh
npx @microsoft/teams.cli@latest new python oauth-app --template graph
```
This command:
1. Creates a new directory called `oauth-app`.
2. Bootstraps the graph agent template files into it under `oauth-app/src`.
3. Creates your agent's manifest files, including a `manifest.json` file and placeholder icons in the `oauth-app/appPackage` directory.
### Add Agents Toolkit auth configuration
Open your terminal with the project folder set as the current working directory and run the following command:
```sh
npx @microsoft/teams.cli config add atk.oauth
```
The `atk.oauth` configuration is a basic setup for Agents Toolkit along with configurations to authenticate the user with Microsoft Entra ID to access Microsoft Graph APIs.
This [CLI](/developer-tools/cli) command adds configuration files required by Agents Toolkit, including:
- Azure Application Entra ID manifest file `aad.manifest.json`.
- Azure bicep files to provision Azure bot in `infra/` folder.
:::info
Agents Toolkit, in the debugging flow, will deploy the `aad.manifest.json` and `infra/azure.local.bicep` file to provision the Application Entra ID and Azure bot with oauth configurations.
:::
## Configure the OAuth connection
```python
from teams import App
from teams.api import MessageActivity, SignInEvent
from teams.apps import ActivityContext
from teams.logger import ConsoleLogger, ConsoleLoggerOptions
app = App(
# The name of the auth connection to use.
# It should be the same as the Oauth connection name defined in the Azure Bot configuration.
default_connection_name="graph",
logger=ConsoleLogger().create_logger("auth", options=ConsoleLoggerOptions(level="debug")))
```
:::tip
Make sure you use the same name you used when creating the OAuth connection in the Azure Bot Service resource.
:::
:::note
In many templates, `graph` is the default name of the OAuth connection, but you can change that by supplying a different connection name in your app configuration.
:::
## Signing In
:::note
This uses the Single Sign-On (SSO) authentication flow. To learn more about all the available flows and their differences see the [official documentation](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0).
:::
You must call the `signin` method inside your route handler, for example: to signin when receiving the `/signin` message:
```python
@app.on_message
async def handle_signin_message(ctx: ActivityContext[MessageActivity]):
"""Handle message activities for signing in."""
ctx.logger.info("User requested sign-in.")
if ctx.is_signed_in:
await ctx.send("You are already signed in.")
else:
await ctx.sign_in()
```
## Subscribe to the SignIn event
You can subscribe to the `signin` event, that will be triggered once the OAuth flow completes.
```python
@app.event("sign_in")
async def handle_sign_in(event: SignInEvent):
"""Handle sign-in events."""
await event.activity_ctx.send("You are now signed in!")
```
## Start using the graph client
From this point, you can use the `IsSignedIn` flag and the `userGraph` client to query graph, for example to reply to the `/whoami` message, or in any other route.
:::note
The default OAuth configuration requests the `User.ReadBasic.All` permission. It is possible to request other permissions by modifying the App Registration for the bot on Azure.
:::
```python
@app.on_message
async def handle_whoami_message(ctx: ActivityContext[MessageActivity]):
"""Handle messages to show user information from Microsoft Graph."""
if not ctx.is_signed_in:
await ctx.send("You are not signed in! Please sign in to continue.")
return
# Access user's Microsoft Graph data
me = await ctx.user_graph.me.get()
await ctx.send(f"Hello {me.display_name}! Your email is {me.mail or me.user_principal_name}")
@app.on_message
async def handle_all_messages(ctx: ActivityContext[MessageActivity]):
"""Handle all other messages."""
if ctx.is_signed_in:
await ctx.send(f'You said: "{ctx.activity.text}". Please type **/whoami** to see your profile or **/signout** to sign out.')
else:
await ctx.send(f'You said: "{ctx.activity.text}". Please type **/signin** to sign in.')
```
## Signing Out
You can signout by calling the `signout` method, this will remove the token from the User Token service cache
```python
@app.on_message
async def handle_signout_message(ctx: ActivityContext[MessageActivity]):
"""Handle sign out requests."""
if not ctx.is_signed_in:
await ctx.send("You are not signed in!")
return
await ctx.sign_out()
await ctx.send("You have been signed out!")
```
## Handling Sign-In Failures
When using SSO, if the token exchange fails Teams sends a `signin/failure` invoke activity to your app. The SDK includes a built-in default handler that logs a warning with actionable troubleshooting guidance. You can optionally register your own handler to customize the behavior:
```python
@app.on_signin_failure()
async def handle_signin_failure(ctx):
failure = ctx.activity.value
print(f"Sign-in failed: {failure.code} - {failure.message}")
await ctx.send("Sign-in failed.")
```
:::note
In Python, registering a custom handler does **not** replace the built-in default handler. Both will run as part of the middleware chain.
:::
:::tip
The most common failure codes are `installedappnotfound` (bot app not installed for the user) and `resourcematchfailed` (Token Exchange URL doesn't match the Application ID URI). See [SSO Setup - Troubleshooting](/teams/user-authentication/sso-setup#troubleshooting-sso) for a full list of failure codes and troubleshooting steps.
:::
## Regional Configs
You may be building a regional bot that is deployed in a specific Azure region (such as West Europe, East US, etc.) rather than global. This is important for organizations that have data residency requirements or want to reduce latency by keeping data and authentication flows within a specific area.
These examples use West Europe, but follow the equivalent for other regions.
To configure a new regional bot in Azure, you must setup your resoures in the desired region. Your resource group must also be in the same region.
1. Deploy a new App Registration in `westeurope`.
2. Deploy and link a new Enterprise Application (Service Principal) on Microsoft Entra in `westeurope`.
3. Deploy and link a new Azure Bot in `westeurope`.
4. In your App Registration, in the `Authentication (Preview)` tab, add a `Redirect URI` for the Platform Type `Web` to your regional endpoint (e.g., `https://europe.token.botframework.com/.auth/web/redirect`)

5. In your `.env` file (or wherever you set your environment variables), add your `OAUTH_URL`. For example:
`OAUTH_URL=https://europe.token.botframework.com`
To configure a new regional bot with ATK, you will need to make a few updates. Note that this assumes you have not yet deployed the bot previously.
1. In `azurebot.bicep`, replace all `global` occurrences to `westeurope`
2. In `manifest.json`, in `validDomains`, `*.botframework.com` should be replaced by `europe.token.botframework.com`
3. In `aad.manifest.json`, replace `https://token.botframework.com/.auth/web/redirect` with `https://europe.token.botframework.com/.auth/web/redirect`
4. In your `.env` file, add your `OAUTH_URL`. For example:
`OAUTH_URL=https://europe.token.botframework.com`.
## Resources
[User Authentication Basics](https://learn.microsoft.com/en-us/azure/bot-service/bot-builder-concept-authentication?view=azure-bot-service-4.0)
---
### 🔗 Link unfurling
# 🔗 Link unfurling
Link unfurling lets your app respond when users paste URLs into Teams. When a URL from your registered domain is pasted, your app receives the URL and can return a card with additional information or actions. This works like a search command where the URL acts as the search term.
:::note
Users can use link unfurling even before they discover or install your app in Teams. This is called [Zero install link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=desktop%2Cjson%2Cadvantages#zero-install-for-link-unfurling). In this scenario, your app will receive a `message.ext.anon-query-link` activity instead of the usual `message.ext.query-link`.
:::
## Setting up your Teams app manifest
### Configure message handlers
```json
"composeExtensions": [
{
"botId": "${{BOT_ID}}",
"messageHandlers": [
{
"type": "link",
"value": {
"domains": [
"www.test.com"
]
}
}
]
}
]
```
### How link unfurling works
When a user pastes a URL from your registered domain (like `www.test.com`) into the Teams compose box, your app will receive a notification. Your app can then respond by returning an adaptive card that displays a preview of the linked content. This preview card appears before the user sends their message in the compose box, allowing them to see how the link will be displayed to others.
```mermaid
flowchart TD
A1["User pastes a URL (e.g., www\.test\.com) in Teams compose box"]
B1([Microsoft Teams])
C1["Your App"]
D1["Adaptive Card Preview"]
A1 --> B1
B1 -->|Sends URL paste notification| C1
C1 -->|Returns card and preview| B1
B1 --> D1
%% Styling for readability and compatibility
style B1 fill:#2E86AB,stroke:#1B4F72,stroke-width:2px,color:#ffffff
style C1 fill:#28B463,stroke:#1D8348,stroke-width:2px,color:#ffffff
style D1 fill:#F39C12,stroke:#D68910,stroke-width:2px,color:#ffffff
```
## Implementing link unfurling
### Handle the query link event
Handle link unfurling when a URL from your registered domain is submitted into the Teams compose box.
```python
from microsoft_teams.api import (
AdaptiveCardAttachment,
MessageExtensionQueryLinkInvokeActivity,
ThumbnailCardAttachment,
card_attachment,
InvokeResponse,
AttachmentLayout,
MessagingExtensionAttachment,
MessagingExtensionInvokeResponse,
MessagingExtensionResult,
MessagingExtensionResultType,
)
from microsoft_teams.apps import ActivityContext
# ...
@app.on_message_ext_query_link
async def handle_message_ext_query_link(ctx: ActivityContext[MessageExtensionQueryLinkInvokeActivity]):
url = ctx.activity.value.url
if not url:
return InvokeResponse[MessagingExtensionInvokeResponse](status400.txt)
card_data = create_link_unfurl_card(url)
main_attachment = card_attachment(AdaptiveCardAttachment(content=card_data["card"]))
preview_attachment = card_attachment(ThumbnailCardAttachment(content=card_data["thumbnail"]))
attachment = MessagingExtensionAttachment(
content_type=main_attachment.content_type,
content=main_attachment.content,
preview=preview_attachment,
)
result = MessagingExtensionResult(
type=MessagingExtensionResultType.RESULT,
attachment_layout=AttachmentLayout.LIST,
attachments=[attachment],
)
return MessagingExtensionInvokeResponse(compose_extension=result)
```
### Create the unfurl card
`create_link_unfurl_card()` function
```python
from typing import Any, Dict
from microsoft_teams.cards import AdaptiveCard
# ...
def create_link_unfurl_card(url: str) -> Dict[str, Any]:
"""Create a card for link unfurling."""
thumbnail = {
"title": "Unfurled Link",
"text": url,
"images": [{"url": IMAGE_URL}],
}
card = AdaptiveCard.model_validate(
{
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "Unfurled Link",
"size": "Large",
"weight": "Bolder",
"color": "Accent",
"style": "heading",
},
{
"type": "TextBlock",
"text": url,
"size": "Small",
"weight": "Lighter",
"color": "Good",
},
],
}
)
return {"card": card, "thumbnail": thumbnail}
```
### User experience flow
The link unfurling response includes both a full adaptive card and a preview card. The preview card appears in the compose box when a user pastes a URL:

The user can expand the preview card by clicking on the _expand_ button on the top right.

The user can then choose to send either the preview or the full adaptive card as a message.
## Resources
- [Link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=desktop%2Cjson%2Cadvantages)
- [Zero install link unfurling](https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/link-unfurling?tabs=desktop%2Cjson%2Cadvantages#zero-install-for-link-unfurling)
---
### Sending Messages
# Sending Messages
Sending messages is a core part of an agent's functionality. With all activity handlers, a `send` method is provided which allows your handlers to send a message back to the user to the relevant conversation.
```python
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
await ctx.send(f"You said '{ctx.activity.text}'")
```
In the above example, the handler gets a `message` activity, and uses the `send` method to send a reply to the user.
```python
@app.event("sign_in")
async def handle_sign_in(event: SignInEvent):
"""Handle sign-in events."""
await event.activity_ctx.send("You are now signed in!")
```
You are not restricted to only replying to `message` activities. In the above example, the handler is listening to `sign_in` events, which are sent when a user successfully signs in.
:::tip
This shows an example of sending a text message. Additionally, you are able to send back things like [adaptive cards](../../in-depth-guides/adaptive-cards) by using the same `send` method. Look at the [adaptive card](../../in-depth-guides/adaptive-cards) section for more details.
:::
## Streaming
You may also stream messages to the user which can be useful for long messages, or AI generated messages. The SDK makes this simple for you by providing a `stream` function which you can use to send messages in chunks.
```python
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
ctx.stream.update("Stream starting...")
await asyncio.sleep(1)
# Stream messages with delays using ctx.stream.emit
for message in STREAM_MESSAGES:
# Add some randomness to timing
await asyncio.sleep(random())
ctx.stream.emit(message)
```
:::note
Streaming is currently only supported in 1:1 conversations, not group chats or channels
:::

## @Mention
Sending a message at `@mentions` a user is as simple including the details of the user using the `add_mention` method
```python
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
await ctx.send(MessageActivityInput(text='hi!').add_mention(account=ctx.activity.from_))
```
## Targeted Messages
:::info[Preview]
Targeted messages are currently in preview.
:::
Targeted messages, also known as ephemeral messages, are delivered to a specific user in a shared conversation. From a single user's perspective, they appear as regular inline messages in a conversation. Other participants won't see these messages, making them useful for authentication flows, help or error responses, personal reminders, or sharing contextual information without cluttering the group conversation.
To send a targeted message when responding to an incoming activity, use the `with_recipient` method with the recipient account and set the targeting flag to true.
```python
from microsoft_teams.api import MessageActivity, MessageActivityInput
from microsoft_teams.apps import ActivityContext
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
# Using with_recipient with is_targeted=True explicitly targets the specified recipient
await ctx.send(
MessageActivityInput(text="This message is only visible to you!")
.with_recipient(ctx.activity.from_, is_targeted=True)
)
```
### Targeted messages in preview
## Reactions
:::info[Preview]
Reactions are currently in preview.
:::
Reactions allow your agent to add or remove emoji reactions on messages in a conversation. The reactions client is available via the API client.
### Reactions in preview
---
### App Authentication
# App Authentication
Your application needs to authenticate to send messages to Teams as your bot. Authentication allows your app service to certify that it is _allowed_ to send messages as your Azure Bot.
:::info Azure Setup Required
Before configuring your application, you must first set up authentication in Azure. See the [App Authentication Setup](/teams/app-authentication) guide for instructions on creating the necessary Azure resources.
:::
## Authentication Methods
There are 3 main ways of authenticating:
1. **Client Secret** - Simple password-based authentication using a client secret
2. **User Managed Identity** - Passwordless authentication using Azure managed identities
3. **Federated Identity Credentials** - Advanced identity federation using managed identities
## Configuration Reference
The Teams SDK automatically detects which authentication method to use based on the environment variables you set:
| CLIENT_ID | CLIENT_SECRET | MANAGED_IDENTITY_CLIENT_ID | Authentication Method |
|-|-|-|-|
| not_set | | | No-Auth (local development only) |
| set | set | | Client Secret |
| set | not_set | | User Managed Identity |
| set | not_set | set (same as CLIENT_ID) | User Managed Identity |
| set | not_set | set (different from CLIENT_ID) | Federated Identity Credentials (UMI) |
| set | not_set | "system" | Federated Identity Credentials (System Identity) |
## Client Secret
The simplest authentication method using a password-like secret.
### Setup
First, complete the [Client Secret Setup](/teams/app-authentication/client-secret) in Azure Portal or Azure CLI.
### Configuration
Set the following environment variables in your application:
- `CLIENT_ID`: Your Application (client) ID
- `CLIENT_SECRET`: The client secret value you created
- `TENANT_ID`: The tenant id where your bot is registered
```env
CLIENT_ID=your-client-id-here
CLIENT_SECRET=your-client-secret-here
TENANT_ID=your-tenant-id
```
The SDK will automatically use Client Secret authentication when both `CLIENT_ID` and `CLIENT_SECRET` are provided.
## User Managed Identity
Passwordless authentication using Azure managed identities - no secrets to rotate or manage.
### Setup
First, complete the [User Managed Identity Setup](/teams/app-authentication/user-managed-identity) in Azure Portal or Azure CLI.
### Configuration
Your application should automatically use User Managed Identity authentication when you provide the `CLIENT_ID` environment variable without a `CLIENT_SECRET`.
## Configuration
Set the following environment variables in your application:
- `CLIENT_ID`: Your Application (client) ID
- **Do not set** `CLIENT_SECRET`
- `TENANT_ID`: The tenant id where your bot is registered
```env
CLIENT_ID=your-client-id-here
# Do not set CLIENT_SECRET
TENANT_ID=your-tenant-id
```
## Federated Identity Credentials
Advanced identity federation allowing you to assign managed identities directly to your App Registration.
### Setup
First, complete the [Federated Identity Credentials Setup](/teams/app-authentication/federated-identity-credentials) in Azure Portal or Azure CLI.
### Configuration
Depending on the type of managed identity you select, set the environment variables accordingly.
**For User Managed Identity:**
Set the following environment variables:
- `CLIENT_ID`: Your Application (client) ID
- `MANAGED_IDENTITY_CLIENT_ID`: The Client ID for the User Managed Identity resource
- **Do not set** `CLIENT_SECRET`
- `TENANT_ID`: The tenant id where your bot is registered
```env
CLIENT_ID=your-app-client-id-here
MANAGED_IDENTITY_CLIENT_ID=your-managed-identity-client-id-here
# Do not set CLIENT_SECRET
TENANT_ID=your-tenant-id
```
**For System Assigned Identity:**
Set the following environment variables:
- `CLIENT_ID`: Your Application (client) ID
- `MANAGED_IDENTITY_CLIENT_ID`: `system`
- **Do not set** `CLIENT_SECRET`
- `TENANT_ID`: The tenant id where your bot is registered
```env
CLIENT_ID=your-app-client-id-here
MANAGED_IDENTITY_CLIENT_ID=system
# Do not set CLIENT_SECRET
TENANT_ID=your-tenant-id
```
## Troubleshooting
If you encounter authentication errors, see the [Authentication Troubleshooting](/teams/app-authentication/troubleshooting) guide for common issues and solutions.
---
### Teams API Client
# Teams API Client
Teams has a number of areas that your application has access to via its API. These are all available via the `app.api` object. Here is a short summary of the different areas:
| Area | Description |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `conversations` | Gives your application the ability to perform activities on conversations (send, update, delete messages, etc.), or create conversations (like 1:1 chat with a user) |
| `meetings` | Gives your application access to meeting details and participant information via `get_by_id` and `get_participant` |
| `teams` | Gives your application access to team or channel details |
An instance of the API client is passed to handlers that can be used to fetch details:
## Example
In this example, we use the API client to fetch the members in a conversation. The `api` object is passed to the activity handler in this case.
```python
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
members = await ctx.api.conversations.members.get(ctx.activity.conversation.id)
```
## Proactive API
It's also possible to access the API client from outside a handler via the app instance. Here we have the same example as above, but we're access the API client via the app instance.
```python
members = await app.api.conversations.members.get("...")
```
## Meetings Example
In this example, we use the API client to get a specific meeting participant's details, such as their role (e.g. Organizer) and whether they are currently in the meeting. Provide the user's AAD Object ID to specify which participant to look up. The `meetingId` and `tenantId` are available from the activity's channel data.
:::note
To retrieve **all** members of a meeting, use the conversations API as shown in the [example above](#example), since meetings are also conversations.
:::
```python
@app.on_activity("meetingStart")
async def handle_meeting_start(ctx: ActivityContext):
meeting_id = ctx.activity.channel_data.meeting.id
tenant_id = ctx.activity.channel_data.tenant.id
user_id = ctx.activity.from_.aad_object_id
if meeting_id and tenant_id and user_id:
participant = await ctx.api.meetings.get_participant(meeting_id, user_id, tenant_id)
# participant.meeting.role — "Organizer", "Presenter", "Attendee"
# participant.meeting.in_meeting — True/False
```
Visit [Meeting Events](../in-depth-guides/meeting-events) to learn more about meeting events.
---
### Feedback
# Feedback
User feedback is essential for the improvement of any application. Teams provides specialized UI components to help facilitate the gathering of feedback from users.

## Storage
Once you receive a feedback event, you can choose to store it in some persistent storage. In the example below, we are storing it in an in-memory store.
Once you receive a feedback event, you can choose to store it in some persistent storage. You'll need to implement storage for tracking:
- Like/dislike counts per message
- Text feedback comments
- Message ID associations
For production applications, consider using databases, file systems, or cloud storage. The examples below use in-memory storage for simplicity.
## Including Feedback Buttons
When sending a message that you want feedback in, simply add feedback functionality to the message you are sending.
```python
from microsoft_teams.ai import Agent
from microsoft_teams.api import MessageActivityInput
from microsoft_teams.apps import ActivityContext, MessageActivity
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
"""Handle 'feedback demo' command to demonstrate feedback collection"""
agent = Agent(current_model)
chat_result = await agent.send(
input="Tell me a short joke",
instructions="You are a comedian. Keep responses brief and funny."
)
if chat_result.response.content:
message = MessageActivityInput(text=chat_result.response.content)
.add_ai_generated()
# Create message with feedback enabled
.add_feedback()
await ctx.send(message)
```
## Handling the feedback
Once the user decides to like/dislike the message, you can handle the feedback in a received event. Once received, you can choose to include it in your persistent store.
```python
from typing import Dict, Any
from microsoft_teams.api import MessageSubmitActionInvokeActivity
from microsoft_teams.apps import ActivityContext
# ...
# Handle feedback submission events
@app.on_message_submit_feedback
async def handle_message_feedback(ctx: ActivityContext[MessageSubmitActionInvokeActivity]):
"""Handle feedback submission events"""
activity = ctx.activity
# Extract feedback data from activity value
if not hasattr(activity, "value") or not activity.value:
logger.warning(f"No value found in activity {activity.id}")
return
# Access feedback data directly from invoke value
invoke_value = activity.value
assert invoke_value.action_name == "feedback"
feedback_str = invoke_value.action_value.feedback
reaction = invoke_value.action_value.reaction
feedback_json: Dict[str, Any] = json.loads(feedback_str)
# { 'feedbackText': 'the ai response was great!' }
if not activity.reply_to_id:
logger.warning(f"No replyToId found for messageId {activity.id}")
return
# Store the feedback (implement your own storage logic)
upsert_feedback_storage(activity.reply_to_id, reaction, feedback_json.get('feedbackText', ''))
# Optionally Send confirmation response
feedback_text: str = feedback_json.get("feedbackText", "")
reaction_text: str = f" and {reaction}" if reaction else ""
text_part: str = f" with comment: '{feedback_text}'" if feedback_text else ""
await ctx.reply(f"✅ Thank you for your feedback{reaction_text}{text_part}!")
```
---
### Graph API Client
# Graph API Client
[Microsoft Graph](https://docs.microsoft.com/en-us/graph/overview) gives you access to the wider Microsoft 365 ecosystem. You can enrich your application with data from across Microsoft 365.
The SDK gives your application easy access to the Microsoft Graph API via the `microsoft-teams-graph` package.
## Calling APIs
Microsoft Graph can be accessed by your application using its own application token, or by using the user's token. If you need access to resources that your application may not have, but your user does, you will need to use the user's scoped graph client. To grant explicit consent for your application to access resources on behalf of a user, follow the [auth guide](../in-depth-guides/user-authentication).
To access the graph using the Graph using the app, you may use the `app.graph` object to call the endpoint of your choice.
```python
# Equivalent of https://learn.microsoft.com/en-us/graph/api/user-get
# Gets the details of the bot-user
user = await app.graph.me.get()
print(f"User ID: {user.id}")
print(f"User Display Name: {user.display_name}")
print(f"User Email: {user.mail}")
print(f"User Job Title: {user.job_title}")
```
You can also access the graph using the user's token from within a message handler via the `user_graph` property.
```python
@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
user = await ctx.user_graph.me.get()
print(f"User ID: {user.id}")
print(f"User Display Name: {user.display_name}")
print(f"User Email: {user.mail}")
print(f"User Job Title: {user.job_title}")
```
Here, the `user_graph` object is a scoped graph client for the user that sent the message.
:::tip
You also have access to the `app_graph` object in the activity handler. This is equivalent to `app.graph`.
:::
---
### Meeting Events
# Meeting Events
Microsoft Teams provides meeting events that allow your application to respond to various meeting lifecycle changes. Your app can listen to events like when a meeting starts, meeting ends, and participant activities to create rich, interactive experiences.
## Overview
Meeting events enable your application to:
- Send notifications when meetings start or end
- Track participant activity (join/leave events)
- Display relevant information or cards based on meeting context
- Integrate with meeting workflows
## Configuring Your Bot
There are a few requirements in the Teams app manifest (`manifest.json`) to support these events.
1. The scopes section must include `team`, and `groupChat`
```json
bots": [
{
"botId": "",
"scopes": [
"team",
"personal",
"groupChat"
],
"isNotificationOnly": false
}
]
```
2. In the authorization section, make sure to specify the following resource-specific permissions:
```json
"authorization":{
"permissions":{
"resourceSpecific":[
{
"name":"OnlineMeetingParticipant.Read.Chat",
"type":"Application"
},
{
"name":"ChannelMeeting.ReadBasic.Group",
"type":"Application"
},
{
"name":"OnlineMeeting.ReadBasic.Chat",
"type":"Application"
}
]
}
}
```
3. In the Teams Developer Portal, for your `Bot`, make sure the `Meeting Event Subscriptions` are checked off. This enables you to receive the Meeting Participant events. For these events, you must create your Bot via TDP.
## Meeting Start Event
When a meeting starts, your app can handle the `meetingStart` event to send a notification or card to the meeting chat.
```python
from microsoft_teams.api.activities.event import MeetingStartEventActivity
from microsoft_teams.apps import ActivityContext, App
from microsoft_teams.cards import AdaptiveCard, OpenUrlAction, TextBlock
app = App()
@app.on_meeting_start
async def handle_meeting_start(ctx: ActivityContext[MeetingStartEventActivity]):
meeting_data = ctx.activity.value
start_time = meeting_data.start_time.strftime("%c")
card = AdaptiveCard(
body=[
TextBlock(
text=f"'{meeting_data.title}' has started at {start_time}.",
wrap=True,
weight="Bolder",
)
],
actions=[OpenUrlAction(url=meeting_data.join_url, title="Join the meeting")],
)
await ctx.send(card)
```
## Meeting End Event
When a meeting ends, your app can handle the `meetingEnd` event to send a summary or follow-up information.
```python
from microsoft_teams.api.activities.event import MeetingEndEventActivity
from microsoft_teams.apps import ActivityContext, App
from microsoft_teams.cards import AdaptiveCard, TextBlock
app = App()
@app.on_meeting_end
async def handle_meeting_end(ctx: ActivityContext[MeetingEndEventActivity]):
meeting_data = ctx.activity.value
end_time = meeting_data.end_time.strftime("%c")
card = AdaptiveCard(
body=[
TextBlock(
text=f"'{meeting_data.title}' has ended at {end_time}.",
wrap=True,
weight="Bolder",
)
]
)
await ctx.send(card)
```
## Participant Join Event
When a participant joins a meeting, your app can handle the `meetingParticipantJoin` event to welcome them or display their role.
```python
from microsoft_teams.api.activities.event import MeetingParticipantJoinEventActivity
from microsoft_teams.apps import ActivityContext, App
from microsoft_teams.cards import AdaptiveCard, TextBlock
app = App()
@app.on_meeting_participant_join
async def handle_meeting_participant_join(ctx: ActivityContext[MeetingParticipantJoinEventActivity]):
meeting_data = ctx.activity.value
member = meeting_data.members[0].user.name
role = meeting_data.members[0].meeting.role if hasattr(meeting_data.members[0].meeting, "role") else "a participant"
card = AdaptiveCard(
body=[
TextBlock(
text=f"{member} has joined the meeting as {role}.",
wrap=True,
weight="Bolder",
)
]
)
await ctx.send(card)
```
## Participant Leave Event
When a participant leaves a meeting, your app can handle the `meetingParticipantLeave` event to notify others.
```python
from microsoft_teams.api.activities.event import MeetingParticipantLeaveEventActivity
from microsoft_teams.apps import ActivityContext, App
from microsoft_teams.cards import AdaptiveCard, TextBlock
app = App()
@app.on_meeting_participant_leave
async def handle_meeting_participant_leave(ctx: ActivityContext[MeetingParticipantLeaveEventActivity]):
meeting_data = ctx.activity.value
member = meeting_data.members[0].user.name
card = AdaptiveCard(
body=[
TextBlock(
text=f"{member} has left the meeting.",
wrap=True,
weight="Bolder",
)
]
)
await ctx.send(card)
```
---
### Observability
# Observability
---