Skip to main content
Create a Microsoft Teams app with a bot. The bot lets Courier send notifications to Teams channels and users.

Prerequisites

You build three things: an Azure app registration for authentication, a Teams app to install, and a bot that sends. Courier only ever holds one set of credentials, the bot’s.

Setup

Steps 1 through 8 are the whole setup for addressing a recipient by conversation_id, channel_id, or user_id. Step 9 adds Graph API permissions, which need an administrator, and only email and channel_name need them.
  1. Navigate to the Microsoft Teams Developer Portal.
  2. Click Create a new app.

Teams App Creation

  1. Enter a name for your app and click Add.

Naming the new app

Add _app to your app name to tell it apart from your bot. This matters most if you manage several apps or bots.
  1. Save the generated App ID for later.
  1. Open the Azure Portal.
  2. Go to Azure Active Directory > App registrations.

Azure App Registration

  1. Click New registration.
  2. Configure the registration:
    • Name: Use the same name as your Teams Developer Portal app
    • Supported account types: Select “Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)”. The Azure default is single-tenant, which causes 401 authentication errors with Bot Framework.
    • Redirect URI: Keep the default setting

Azure App Configuration

  1. Click Register.
  2. Save your Application (client) ID for later.

Grab your Azure AppID

  1. In the Microsoft Teams Developer Portal, go to Tools > Bot Management.

Create new bot for Teams

  1. Click + New Bot and provide a unique name.

Naming your new bot

Use a distinct name for your bot to avoid confusion with your app.
  1. Return to Apps > [Your App Name] > Configure > App Features
  2. Click the Bot item
  3. Select Select an existing bot

Select your newly created bot

  1. Choose the bot you created
  2. Configure the bot settings:
    • Under What can your bot do?, select:
      • ✓ Only send notifications (one-way conversations)
    • Under Select the scopes where people can use your bot, select:
      • ✓ Personal (for 1:1 notifications)
      • ✓ Team (for channel notifications)
      • ✓ Group Chat (for group chat notifications)

Select the scope for your bot

  1. Click Save
  2. Configure API permissions, only to address recipients by email or channel_name. Courier calls Microsoft Graph to turn those into an id, and that call needs an administrator’s consent. Skip this step if you send by conversation_id, channel_id, or user_id.
    • Return to the Azure Portal.
    • Go to App registrations > [Your Bot Name] > API permissions
    • Add these Microsoft Graph permissions as Application permissions:
      • ChannelSettings.Read.All
      • TeamSettings.Read.All
      • User.Read.All
    • Ask an administrator to click Grant admin consent for [your_domain]

Add API permissions

  1. Generate bot credentials:
    • In Azure, go to App registrations > [Your Bot Name] > Certificates & secrets
    • Click + New client secret

Create a new client secret

  • Name it and select an expiration period
  • Click Create
  • Important: copy and store the generated secret value immediately
  • Go to the Overview section
  • Copy and store the Application (client) ID alongside your secret

Get the ApplicationID from Azure (i.e. BotID)

  1. Head to the in Courier.
  2. Enter your Azure credentials from Step 4. Courier’s field labels do not match Azure’s names, so map them carefully:

Add teams to your Courier Account

  1. Click Install Provider.
Courier’s field names do not match Azure’s.
Paste your Azure Application (client) ID into App ID. Your client secret value, the Value and not the Secret ID, goes into App Password. Both come from the Azure Bot App Registration in Step 4.
Find both values in the Azure Bot App Registration from Step 4:
  • Azure Portal > App registrations > [Your Bot Name]: “Application (client) ID”
  • Azure Portal > App registrations > [Your Bot Name] > Certificates & secrets: “Client secrets”
Install the App in Teams:
  1. Return to Apps > your app
  2. Click Publish. When prompted, select “Download the app package”
    • Note: fill in both the short and long app descriptions under Configure > Basic information. The app manifest requires them, and the app fails to install in Teams without them.

Publish your app

  1. Go to Microsoft Teams
  2. Go to Apps and select “Manage your apps” at the bottom

Manage your app

  1. Click “Upload an app” and select your downloaded app package

Upload your app

  1. When prompted, click “Add”
Uploading the app only covers 1:1 messaging. Channel messages fail with a 401 until the app is installed in each team, even with correct credentials.
To install it, open the target team in Microsoft Teams and click the menu beside its name. Choose Manage team, open the Apps tab, find your app, and click Add.
  1. Courier can now send messages to your channels and team members
Get channel identifiers (for testing):
  1. Open Microsoft Teams
  2. Hover over a channel, click the ellipsis icon, and select “Copy link”.
  3. You get a URL like: https://teams.microsoft.com/l/channel/19%3A5140d7460868414cac958ac76a0a94d0%40thread.skype/slack-teams-test?groupId=feb55fc1-9e00-40f3-93b8-f7d14703f4dd&tenantId=dabd1935-56a4-4305-938e-0840e2e84515
  4. Paste it into a URL decoder such as Eric Meyer’s dencoder
  5. Copy the group ID, the team’s identifier. Here: feb55fc1-9e00-40f3-93b8-f7d14703f4dd
  6. Copy the channel name. Here: slack-teams-test
  7. Copy the tenant ID. Here: dabd1935-56a4-4305-938e-0840e2e84515

Get channel name

Send a test message:
  1. Return to and create a new template.
  2. Select the Teams provider you configured in Step 5
  3. In the “Design” tab, write a basic text message. See for the designer.
  4. Click the Preview tab. Create a “Test event”, a request you can use to send a message.
  5. Click “Create test event” and enter this, using your own values:
TestEvent
  1. Click “Publish”
  2. Go to the Send tab. Click “Send Test”

Profile requirements

The recipient’s profile needs an ms_teams object with:
  • service_url, optional. Defaults to https://smba.trafficmanager.net/amer, so set it only if your tenant is outside the Americas.
  • tenant_id, required for email and channel_name (see Using a tenant_id). Courier also reads it from the service_url path.
  • One of these identifiers:
    • user_id
    • email
    • channel_id
    • conversation_id
    • Combination of team_id and channel_name
    • Thread reply fields: reply_to_activity_id and conversation_id
Open teams.microsoft.com/?tenantId and copy tenantId from the redirected URL. If it is missing, use the three-dot menu beside your Team, choose Get link to team, and read it from that URL.
Locate Tenant ID in Microsoft Teams
For users in the Americas region, the standard service URL is https://smba.trafficmanager.net/amer.
To send with email or channel_name, your bot needs these Microsoft Graph API permissions:
  • ChannelSettings.Read.All (requires admin consent)
  • TeamSettings.Read.All (requires admin consent)
  • User.Read.All
Courier uses them to resolve the user_id or channel_id through the Microsoft Graph API.

Using a tenant_id

Courier requires tenant_id for the two shapes that need a Microsoft Graph lookup:
  • email, which Courier resolves to a user id
  • channel_name, which Courier resolves to a channel id
Without it, those sends fail with tenant_id is required when using 'email' or 'channel_name' for lookups. Send it on a user_id too, since Teams needs it to open a personal conversation. Courier also reads tenant_id from the service_url path segments. Set it on service_url, as a body parameter, or both when the values match.
tenant_id appears on provider:sent and provider:delivered event payloads only when the request includes it, because that is the only time Courier knows it. Replies still work without it.
When your bot is registered in a different Azure tenant from the recipient’s, add source_tenant_id for the bot’s home tenant. tenant_id still routes the message, and source_tenant_id authenticates the bot. Setting it without tenant_id fails. Store whichever shape you picked on the profile. Store the ms_teams object once with , which merges into the profile and creates it if it does not exist:
The other four shapes go on the profile the same way. Swap the identifier and keep tenant_id and service_url. Then send to user_id and Courier resolves the address.

Send by user id

The call in every language, and the rest of the profile object.

Send to a recipient

Courier reads ms_teams off the saved profile, so preferences apply and the value can change without touching this code.

Thread replies

Courier can post a message as a reply to an existing channel message instead of a new root-level message. Every send returns reference data in the provider:sent event, an activityId and a conversationId. Use those values to reply to that message in a thread.
The flow:
  1. Send a root-level message to any Teams channel
  2. Look up the sent event (via API, webhook, or Courier UI) to get the activityId and conversationId
  3. Use those identifiers to send channel thread replies
  • Channels only. A Microsoft platform limit: thread replies work in channels, not in 1:1 or group chats. Standard, private, and shared channels all work.
  • Reference data required. You need the activityId and conversationId from a previous message’s provider:sent event.
  • Validation. Courier rejects invalid field combinations with a clear error.
Example workflow
1

Send the initial message

2

Get reference data from the provider:sent event

The provider:sent event carries the reference data. Read it from:
  • Courier API: query message details for the provider:sent event data
  • Courier UI: view message details in the Messages section of your dashboard
  • Webhooks (if configured): receive the reference data on every send
Example reference data from the provider:sent event:
3

Send the thread reply

Getting reference dataWith webhooks configured, Courier delivers the reference data on every send:
Thread reply requirements
  1. Required fields:
    • reply_to_activity_id: the activity ID from the original message’s reference data
    • conversation_id: The conversation ID from the original message’s reference data
    • tenant_id: your Microsoft Teams tenant ID
    • service_url: your Teams service URL (optional, uses the default if omitted)
  2. Field restrictions: With reply_to_activity_id, you cannot include:
    • channel_id or channel_name + team_id (channel targeting fields)
    • user_id or email (user targeting fields)
    The conversation_id determines the thread location automatically.
Error handlingCommon threading errors and their fixes:
  • “Thread replies require ‘conversation_id’”: include the conversation_id from the original message’s reference data.
  • “Thread replies cannot use channel targeting fields”: remove channel_id, channel_name, and team_id.
  • “Thread replies cannot use user targeting fields”: remove user_id and email.

Microsoft Teams channel ID reference

When sending messages to a Teams channel through Courier, the conversationId returned in delivered event webhooks is the channel ID. They are the same value.
In the Microsoft Teams Bot Framework, a channel’s identifier is its conversation ID. The Bot Framework returns that identifier in the response, and Courier surfaces it as conversation_context.conversationId in delivered event webhooks.
The delivered event webhook contains:
The conversationId value 19:a52e21710de34c65b9f2e09ededaad2a@thread.skype is the channel ID for the “General” channel.

Replying in a thread

To reply to this message, use both the activityId and conversationId from the webhook:
The reply’s delivered event maintains the same conversationId:
The conversationId stays the same across a thread because it represents the channel. Only the activityId changes with each message.
You can get a channel ID without sending a message first. In the Teams web app, select the channel and click the ellipsis (⋯) in the upper right corner. Choose “Get link to channel” to copy a URL like:
The channel ID appears after /channel/ in URL-encoded format. Decode 19%3Aa52e21710de34c65b9f2e09ededaad2a%40thread.skype to get 19:a52e21710de34c65b9f2e09ededaad2a@thread.skype.
When you know the channel ID, send with conversation_id instead of looking up the channel by name:
This skips the Graph API lookup for the channel name, so it is faster. The webhook returns the same conversationId because it is already the channel ID.
Channel IDs follow the format 19:{guid}@thread.skype or 19:{guid}@thread.tacv2, depending on the Teams infrastructure version. They are stable and unique per channel within a tenant. Combined with tenantId, they identify a channel globally.The Bot Framework uses these channel IDs as conversation identifiers when posting to channels. Thread replies need both the conversationId (channel ID) and the reply_to_activity_id (the message being replied to). The reply URL uses the format {conversationId};messageid={activityId}, but the conversationId stored in the response is the channel ID with no suffix.

Overrides

covers the two levels and which one wins. A config override swaps the Azure Bot’s App ID and App Password for one send:

Adaptive Cards

Courier supports Microsoft Teams Adaptive Cards through Jsonnet blocks in Design Studio.
Jsonnet blocks let you customize Adaptive Cards sent through Microsoft Teams. To send your first card:
1

Add a Microsoft Teams channel

In Design Studio, add a new Microsoft Teams channel.
2

Insert a Jsonnet block

Insert a Jsonnet block within your message.

Jsonnet Block

3

Open the Adaptive Cards Designer

Open the Adaptive Cards Designer and select or create a card layout.
4

Copy the card JSON into your Jsonnet block

Copy the card JSON from the “Card Payload Editor” into your Courier Jsonnet block.

Sample Jsonnet

Include the sample data from the Adaptive Cards Designer in your Courier message:
The recipient’s profile needs a valid ms_teams object with the right fields, such as conversation_id. The card then appears in Microsoft Teams:

MS Teams Adaptive Card

Mentions in Adaptive Cards need two things:
  • <at>username</at> in the Jsonnet block.
  • A matching entities object in the Adaptive Card JSON payload, with the Teams user ID of the person mentioned.
A sample mention:

Troubleshooting

401 Authentication error

If you see All Bot Framework authentication methods failed with status code 401:
  1. App not installed in the team (channel messages only): uploading the app to Teams covers personal messaging. For channel messaging, install the app in each team. See Step 6. This is the most common cause when user messages succeed and channel messages fail.
  2. Wrong credentials in Courier: Courier needs the Application (client) ID and Client Secret from the Azure Bot App Registration (Step 4), not the Teams App ID from the Developer Portal (Step 1). These are different values.
  3. Single-tenant App Registration: in Azure Portal > App registrations > [Your Bot] > Authentication, set “Supported account types” to “Accounts in any organizational directory” (Multitenant). The Azure default is single-tenant, which breaks Bot Framework authentication.
  4. Expired client secret: check the expiration date in Azure Portal > App registrations > [Your Bot] > Certificates & secrets. If it expired, create a new secret and update Courier’s Teams integration settings.
  5. Missing tenant_id: include tenant_id in the ms_teams object. Without it, Courier cannot attempt tenant-scoped authentication.

Messages to users work but channel messages fail

The app is almost certainly not installed in the team that owns the target channel. See item 1 above and Step 6.

Credential confusion between Azure app registrations

Setup creates two Azure App Registrations:
  • Step 2 App Registration: used in the Teams Developer Portal configuration (Step 3). This is the parent application.
  • Step 4 Bot App Registration: used in Courier’s integration settings (Step 5). This provides the bot credentials.
Check the Info callouts in Steps 3 and 5 if you are unsure which credentials go where.

Reference: Courier → recipient flowchart

Provider details

Courier recommends routing to the channel. Naming this key in routing.channels instead is supported, and sends through just this provider.

Send to a specific provider

When that is worth doing, and what you give up: failover, channel priority, and providers you add later.