> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message`, the default import of the v7 Node SDK. Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Send Microsoft Teams messages through Courier

> Connect a Teams bot, send to users and channels, reply in threads, and send Adaptive Cards.

export const Endpoint = ({method, path, name, href, children, bare}) => {
  const verb = String(method || "").toUpperCase();
  const title = verb + " " + path;
  const label = children || name || path;
  if (bare) {
    return href ? <a href={href}><code>{title}</code></a> : <code>{title}</code>;
  }
  if (!href) {
    return <span className="cx-endpoint" data-method={verb} title={title}>
        <span className="cx-endpoint-label">{label}</span>
        <span className="cx-endpoint-method">{verb}</span>
      </span>;
  }
  return <a className="cx-endpoint" data-method={verb} href={href} title={title}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">{verb}</span>
    </a>;
};

export const AppLink = ({href, children, name, bare}) => {
  const label = children || name || "Open in Courier";
  if (bare) {
    return <a href={href} target="_blank" rel="noreferrer">{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="app" href={href} target="_blank" rel="noreferrer">
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method" aria-hidden="true">↗</span>
    </a>;
};

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

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

## Prerequisites

* [An account on the Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/)
* [An Azure portal account with App Registration permissions](https://portal.azure.com/)
* An administrator to grant API permissions, but only to address recipients by `email` or `channel_name`
* <AppLink href="https://app.courier.com/">A Courier workspace</AppLink>

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.

<AccordionGroup>
  <Accordion title="Step 1: Create the Teams app">
    1. Navigate to the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/).
    2. Click **Create a new app**.

    <Frame caption="Teams App Creation">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-new-app.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=29ac21e615f40613ba6e930244e54e11" width="1020" height="980" data-path="assets/teams-new-app.webp" />
    </Frame>

    3. Enter a name for your app and click **Add**.

    <Frame caption="Naming the new app">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-name-the-app.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=cd1df9a2e290ab51364400508fe4d32a" width="1414" height="886" data-path="assets/teams-name-the-app.webp" />
    </Frame>

    <Tip>
      Add `_app` to your app name to tell it apart from your bot. This matters most if you manage several apps or bots.
    </Tip>

    4. Save the generated **App ID** for later.
  </Accordion>

  <Accordion title="Step 2: Create the Azure app">
    1. Open the [Azure Portal](https://portal.azure.com/).
    2. Go to **Azure Active Directory > App registrations**.

    <Frame caption="Azure App Registration">
      <img src="https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/azure-app-registration.webp?fit=max&auto=format&n=LdpdyPjJHKHJqFY9&q=85&s=20a30b3ebee6271a79d4f619edc179c7" width="354" height="372" data-path="assets/azure-app-registration.webp" />
    </Frame>

    3. Click **New registration**.
    4. 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

    <Frame caption="Azure App Configuration">
      <img src="https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/azure-app-config.webp?fit=max&auto=format&n=LdpdyPjJHKHJqFY9&q=85&s=ab401a99e1c1f05bdcbff67ca4786171" width="792" height="641" data-path="assets/azure-app-config.webp" />
    </Frame>

    5. Click **Register**.
    6. Save your **Application (client) ID** for later.

    <Frame caption="Grab your Azure AppID">
      <img src="https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/azure-app-id.webp?fit=max&auto=format&n=LdpdyPjJHKHJqFY9&q=85&s=53c4dffd804d2551705e44ca1d5f79bf" width="901" height="401" data-path="assets/azure-app-id.webp" />
    </Frame>
  </Accordion>

  <Accordion title="Step 3: Link the Teams app to the Azure app">
    1. Return to the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/).
    2. Go to **Configure > Basic Information**.
    3. Scroll to the bottom of the page to the **Application (client) ID** field.

    <Frame caption="Teams and Azure app linking">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-azure-app-linking.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=bffeafa1262079dbfa6dcdc384e5237f" width="1135" height="251" data-path="assets/teams-azure-app-linking.webp" />
    </Frame>

    4. Paste the Application (client) ID you saved from Step 2.
    5. Click **Save**.

    <Info>
      This Application (client) ID comes from your Azure App Registration (Step 2). Courier uses different credentials (Step 5), from the Bot's Azure App Registration (Step 4).
    </Info>
  </Accordion>

  <Accordion title="Step 4: Create the bot and set permissions">
    1. In the [Microsoft Teams Developer Portal](https://dev.teams.microsoft.com/), go to **Tools > Bot Management**.

    <Frame caption="Create new bot for Teams">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-create-bot.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=091d6b656bd70c9f6ad0bd6c95984726" width="1686" height="293" data-path="assets/teams-create-bot.webp" />
    </Frame>

    2. Click **+ New Bot** and provide a unique name.

    <Frame caption="Naming your new bot">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-name-the-bot.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=ed10b56602dde10f3963432ea911bae9" width="1234" height="454" data-path="assets/teams-name-the-bot.webp" />
    </Frame>

    <Tip>
      Use a distinct name for your bot to avoid confusion with your app.
    </Tip>

    3. Return to **Apps > \[Your App Name] > Configure > App Features**
    4. Click the **Bot** item
    5. Select **Select an existing bot**

    <Frame caption="Select your newly created bot">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-bot-selection.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=3d3dcc5e729758e1f33ef51cf4fb2b59" width="1164" height="852" data-path="assets/teams-bot-selection.webp" />
    </Frame>

    6. Choose the bot you created
    7. 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)

    <Frame caption="Select the scope for your bot">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-bot-scopes.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=107ed039cec0dda8a7769fb78ae7c4e3" width="676" height="544" data-path="assets/teams-bot-scopes.webp" />
    </Frame>

    8. Click **Save**

    9. **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](https://portal.azure.com/).
       * 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]**

    <Frame caption="Add API permissions">
      <img src="https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/azure-permissions.webp?fit=max&auto=format&n=LdpdyPjJHKHJqFY9&q=85&s=1e23b305ce1f5b857c6b7b9448b6e172" width="1033" height="320" data-path="assets/azure-permissions.webp" />
    </Frame>

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

    <Frame caption="Create a new client secret">
      <img src="https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/azure-new-secret.webp?fit=max&auto=format&n=LdpdyPjJHKHJqFY9&q=85&s=c451215aad303d9851e4597e1f0c4bbf" width="1125" height="352" data-path="assets/azure-new-secret.webp" />
    </Frame>

    * 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

    <Frame caption="Get the ApplicationID from Azure (i.e. BotID)">
      <img src="https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/azure-bot-id.webp?fit=max&auto=format&n=LdpdyPjJHKHJqFY9&q=85&s=dccd34022310be95d82aa7fc7583a38e" width="887" height="394" data-path="assets/azure-bot-id.webp" />
    </Frame>
  </Accordion>

  <Accordion title="Step 5: Configure in Courier">
    1. Head to the <AppLink href="https://app.courier.com/integrations/catalog/msteams">Teams Integration</AppLink> in Courier.
    2. Enter your Azure credentials from Step 4. Courier's field labels do not match Azure's names, so map them carefully:

    <Frame caption="Add teams to your Courier Account">
      <img src="https://mintcdn.com/courier-4f1f25dc/LdpdyPjJHKHJqFY9/assets/courier-adding-teams.webp?fit=max&auto=format&n=LdpdyPjJHKHJqFY9&q=85&s=cddaabb2d6dc644f7cacc6baef5db76b" width="2116" height="1366" data-path="assets/courier-adding-teams.webp" />
    </Frame>

    3. Click **Install Provider**.

    <Warning>
      **Courier's field names do not match Azure's.**<br />
      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.
    </Warning>

    <Info>
      Find both values in the Azure Bot App Registration from Step 4:

      * [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps) > **App registrations > \[Your Bot Name]**: *"Application (client) ID"*
      * [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps) > **App registrations > \[Your Bot Name] > Certificates & secrets**: *"Client secrets"*
    </Info>
  </Accordion>

  <Accordion title="Step 6: Install the app and test">
    **Install the App in Teams:**

    1. Return to [Apps > your app](https://dev.teams.microsoft.com/apps)
    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.

    <Frame caption="Publish your app">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-publish-app.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=9f11985484a9a6e6dadffd9fa8afe614" width="783" height="579" data-path="assets/teams-publish-app.webp" />
    </Frame>

    3. Go to [Microsoft Teams](https://teams.microsoft.com/)
    4. Go to **Apps** and select **"Manage your apps"** at the bottom

    <Frame caption="Manage your app">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-manage-app.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=536cd52cafcc745ecaa2fd5f1ff9e46b" width="723" height="264" data-path="assets/teams-manage-app.webp" />
    </Frame>

    5. Click **"Upload an app"** and select your downloaded app package

    <Frame caption="Upload your app">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-upload-app.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=ff648c0fb54689ba0c36a7dd29c18ee4" width="993" height="840" data-path="assets/teams-upload-app.webp" />
    </Frame>

    6. When prompted, click **"Add"**

    <Warning>
      **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.
    </Warning>

    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**.

    7. Courier can now send messages to your channels and team members

    **Get channel identifiers (for testing):**

    1. Open [Microsoft Teams](https://teams.microsoft.com/)
    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](https://meyerweb.com/eric/tools/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`

    <Frame caption="Get channel name">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-get-channel-name.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=7b1a00a45610e8aaec55a3ec9f1faa77" width="417" height="223" data-path="assets/teams-get-channel-name.webp" />
    </Frame>

    **Send a test message:**

    1. Return to <AppLink href="https://app.courier.com/assets/templates">Courier</AppLink> 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 <Doc href="/docs/design/templates/overview">how templates work</Doc> 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:

    ```json TestEvent theme={null}
    {
      "courier": {},
      "data": {},
      "profile": {
        "ms_teams": {
          "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
          "channel_name": "slack-teams-test",
          "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
          "service_url": "https://smba.trafficmanager.net/amer"
        }
      },
      "override": {},
      "meta": {}
    }
    ```

    6. Click **"Publish"**
    7. Go to the **Send** tab. Click **"Send Test"**
  </Accordion>
</AccordionGroup>

## 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](#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`

<CodeGroup>
  ```json user_id highlight={5} theme={null}
  {
    "message": {
      "to": {
        "ms_teams": {
          "user_id": "<user_id>",
          "tenant_id": "<tenant_id>",
          "service_url": "https://smba.trafficmanager.net/amer"
        }
      }
    }
  }
  ```

  ```json email highlight={5} theme={null}
  {
    "message": {
      "to": {
        "ms_teams": {
          "email": "<user_email>",
          "tenant_id": "<tenant_id>",
          "service_url": "https://smba.trafficmanager.net/amer"
        }
      }
    }
  }
  ```

  ```json conversation_id highlight={5} theme={null}
  {
    "message": {
      "to": {
        "ms_teams": {
          "conversation_id": "<conversation_id>",
          "tenant_id": "<tenant_id>",
          "service_url": "https://smba.trafficmanager.net/amer"
        }
      }
    }
  }
  ```

  ```json team_id+channel_name highlight={5-6} theme={null}
  {
    "message": {
      "to": {
        "ms_teams": {
          "team_id": "<team_id>",
          "channel_name": "<channel_name>",
          "tenant_id": "<tenant_id>",
          "service_url": "https://smba.trafficmanager.net/amer"
        }
      }
    }
  }
  ```

  ```json thread_reply highlight={5-6} theme={null}
  {
    "message": {
      "to": {
        "ms_teams": {
          "reply_to_activity_id": "<activity_id>",
          "conversation_id": "<conversation_id>",
          "tenant_id": "<tenant_id>",
          "service_url": "https://smba.trafficmanager.net/amer"
        }
      }
    }
  }
  ```
</CodeGroup>

<Info>
  Open [teams.microsoft.com/?tenantId](https://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.

  <Frame>
    <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-tenant-id.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=1fff5413adb4e6c92eeccd5cb5264331" alt="Locate Tenant ID in Microsoft Teams" width="2880" height="1645" data-path="assets/teams-tenant-id.webp" />
  </Frame>
</Info>

<Info>
  For users in the Americas region, the standard service URL is `https://smba.trafficmanager.net/amer`.
</Info>

<Info>
  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.
</Info>

### 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.

```bash theme={null}
service_url: https://smba.trafficmanager.net/amer/{tenant_id}
tenant_id: {tenant_id}
```

<Info>
  `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.
</Info>

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 <Endpoint method="POST" path="/profiles/{user_id}" name="Create a Profile" href="/docs/api-reference/user-profiles/create-a-profile" />, which merges into the profile and creates it if it does not exist:

<CodeGroup>
  ```javascript Node.js highlight={3-7} theme={null}
  const profile = await client.profiles.create('user_123', {
    profile: {
      ms_teams: {
        user_id: '<user_id>',
        tenant_id: '<tenant_id>',
        service_url: 'https://smba.trafficmanager.net/amer',
      },
    },
  });
  ```

  ```python Python highlight={4-8} theme={null}
  profile = client.profiles.create(
      user_id="user_123",
      profile={
          "ms_teams": {
              "user_id": "<user_id>",
              "tenant_id": "<tenant_id>",
              "service_url": "https://smba.trafficmanager.net/amer",
          },
      },
  )
  ```

  ```bash cURL highlight={7-11} theme={null}
  curl --request POST \
    --url https://api.courier.com/profiles/user_123 \
    --header "Authorization: Bearer $COURIER_API_KEY" \
    --header 'Content-Type: application/json' \
    --data '{
      "profile": {
        "ms_teams": {
          "user_id": "<user_id>",
          "tenant_id": "<tenant_id>",
          "service_url": "https://smba.trafficmanager.net/amer"
        }
      }
    }'
  ```

  ```ruby Ruby highlight={4-8} theme={null}
  profile = courier.profiles.create(
    "user_123",
    profile: {
      ms_teams: {
        user_id: "<user_id>",
        tenant_id: "<tenant_id>",
        service_url: "https://smba.trafficmanager.net/amer"
      }
    }
  )
  ```

  ```go Go highlight={6-10} theme={null}
  profile, err := client.Profiles.New(
  	context.TODO(),
  	"user_123",
  	courier.ProfileNewParams{
  		Profile: map[string]any{
  			"ms_teams": map[string]any{
  				"user_id": "<user_id>",
  				"tenant_id": "<tenant_id>",
  				"service_url": "https://smba.trafficmanager.net/amer",
  			},
  		},
  	},
  )
  ```

  ```java Java highlight={4-7} theme={null}
  ProfileCreateParams params = ProfileCreateParams.builder()
      .userId("user_123")
      .profile(ProfileCreateParams.Profile.builder()
          .putAdditionalProperty("ms_teams", JsonValue.from(java.util.Map.of(
              "user_id", "<user_id>",
              "tenant_id", "<tenant_id>",
              "service_url", "https://smba.trafficmanager.net/amer")))
          .build())
      .build();
  ProfileCreateResponse profile = client.profiles().create(params);
  ```

  ```php PHP highlight={2-6} theme={null}
  $profile = $client->profiles->create('user_123', profile: [
    'ms_teams' => [
      'user_id' => '<user_id>',
      'tenant_id' => '<tenant_id>',
      'service_url' => 'https://smba.trafficmanager.net/amer',
    ],
  ]);
  ```

  ```csharp C# highlight={7-11} theme={null}
  ProfileCreateParams parameters = new()
  {
      UserID = "user_123",
      Profile = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
          """
          {
            "ms_teams": {
              "user_id": "<user_id>",
              "tenant_id": "<tenant_id>",
              "service_url": "https://smba.trafficmanager.net/amer"
            }
          }
          """
      ),
  };
  var profile = await client.Profiles.Create(parameters);
  ```

  ```bash CLI highlight={4} wrap theme={null}
  courier profiles create \
    --api-key "$COURIER_API_KEY" \
    --user-id user_123 \
    --profile '{"ms_teams":{"user_id":"<user_id>","tenant_id":"<tenant_id>","service_url":"https://smba.trafficmanager.net/amer"}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, save the Microsoft Teams user id and tenant id on user_123.
  ```
</CodeGroup>

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.

<CardGroup cols={1}>
  <Card title="Send by user id" icon="user" href="/docs/recipients/overview#send-by-user-id">
    The call in every language, and the rest of the profile object.
  </Card>
</CardGroup>

## Send to a recipient

<Tabs>
  <Tab title="Send to user id">
    Courier reads `ms_teams` off the saved profile, so preferences apply and the value can change without touching this code.

    <CodeGroup>
      ```javascript Node.js highlight={4} theme={null}
      const { requestId } = await courier.send.message({
        message: {
          to: {
            user_id: "user_123",
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
        },
      });
      ```

      ```python Python highlight={4} theme={null}
      response = client.send.message(
          message={
              "to": {
                  "user_id": "user_123",
              },
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      )
      ```

      ```bash cURL highlight={7} wrap theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "to": {
              "user_id": "user_123"
            },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40"
          }
        }'
      ```

      ```ruby Ruby highlight={4} theme={null}
      response = courier.send_.message(
        message: {
          to: {
            user_id: "user_123"
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40"
        }
      )
      ```

      ```go Go highlight={5} theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{
      			OfUserRecipient: &shared.UserRecipientParam{
      				UserID: courier.String("user_123"),
      			},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      	},
      })
      ```

      ```java Java highlight={3} theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(UserRecipient.builder().userId("user_123").build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .build())
          .build();
      SendMessageResponse response = client.send().message(params);
      ```

      ```php PHP highlight={4} theme={null}
      $response = $client->send->message(
        message: [
          'to' => [
            'user_id' => 'user_123',
          ],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
        ],
      );
      ```

      ```csharp C# highlight={5} theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { UserID = "user_123" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      };

      var response = await client.Send.Message(parameters);
      ```

      ```bash CLI highlight={3} wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"user_id": "user_123"}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40
      ```

      ```text MCP theme={null}
      With Courier MCP, send my template to user_123 by email.
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Send to `ms_teams`">
    Pass it inline instead and nothing is stored. Swap this `to` object into the call on the other tab.

    <CodeGroup>
      ```javascript Node.js highlight={4-8} theme={null}
      const { requestId } = await courier.send.message({
        message: {
          to: {
            ms_teams: {
              email: "sarah@acme-corp.com",
              service_url: "https://smba.trafficmanager.net/amer",
              tenant_id: "dabd1935-56a4-4305-938e-0840e2e84515",
            },
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
        },
      });
      ```

      ```python Python highlight={4-8} theme={null}
      response = client.send.message(
          message={
              "to": {
                  "ms_teams": {
                      "email": "sarah@acme-corp.com",
                      "service_url": "https://smba.trafficmanager.net/amer",
                      "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
                  },
              },
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      )
      ```

      ```bash cURL highlight={7-11} wrap theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "to": {
              "ms_teams": {
                "email": "sarah@acme-corp.com",
                "service_url": "https://smba.trafficmanager.net/amer",
                "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515"
              }
            },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40"
          }
        }'
      ```

      ```ruby Ruby highlight={4-8} theme={null}
      response = courier.send_.message(
        message: {
          to: {
            ms_teams: {
              email: "sarah@acme-corp.com",
              service_url: "https://smba.trafficmanager.net/amer",
              tenant_id: "dabd1935-56a4-4305-938e-0840e2e84515"
            }
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40"
        }
      )
      ```

      ```go Go highlight={5-9} theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{
      			OfMsTeamsRecipient: &shared.MsTeamsRecipientParam{
      				MsTeams: shared.MsTeamsParamOfSendToMsTeamsEmail(
      					"sarah@acme-corp.com",
      					"https://smba.trafficmanager.net/amer",
      					"dabd1935-56a4-4305-938e-0840e2e84515",
      				),
      			},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      	},
      })
      ```

      ```java Java highlight={4-8} theme={null}
      client.send().message(SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(MsTeamsRecipient.builder()
                  .msTeams(SendToMsTeamsEmail.builder()
                      .email("sarah@acme-corp.com")
                      .serviceUrl("https://smba.trafficmanager.net/amer")
                      .tenantId("dabd1935-56a4-4305-938e-0840e2e84515")
                      .build())
                  .build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .build())
          .build());
      ```

      ```php PHP highlight={4-8} theme={null}
      $response = $client->send->message(
        message: [
          'to' => [
            'ms_teams' => [
              'email' => 'sarah@acme-corp.com',
              'service_url' => 'https://smba.trafficmanager.net/amer',
              'tenant_id' => 'dabd1935-56a4-4305-938e-0840e2e84515',
            ],
          ],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
        ],
      );
      ```

      ```csharp C# highlight={7-12} theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new MsTeamsRecipient
              {
                  MsTeams = new SendToMsTeamsEmail
                  {
                      Email = "sarah@acme-corp.com",
                      ServiceUrl = "https://smba.trafficmanager.net/amer",
                      TenantID = "dabd1935-56a4-4305-938e-0840e2e84515",
                  },
              },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      };

      var response = await client.Send.Message(parameters);
      ```

      ```bash CLI highlight={3} wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"ms_teams": {"email": "sarah@acme-corp.com", "service_url": "https://smba.trafficmanager.net/amer", "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515"}}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40
      ```

      ```text MCP theme={null}
      With Courier MCP, send my template to sarah@acme-corp.com on Microsoft Teams.
      ```
    </CodeGroup>
  </Tab>
</Tabs>

## 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.

<AccordionGroup>
  <Accordion title="Thread reply overview">
    **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**

    <Steps>
      <Step title="Send the initial message">
        <CodeGroup>
          ```javascript Node.js theme={null}
          const { requestId } = await courier.send.message({
            message: {
              template: "nt_01kx4h2jdafq8bk9aftxak4b40",
              to: {
                ms_teams: {
                  tenant_id: "your-tenant-id",
                  service_url: "https://smba.trafficmanager.net/amer",
                  team_id: "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                  channel_name: "general",
                },
              },
            },
          });
          ```

          ```python Python theme={null}
          response = client.send.message(
              message={
                  "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
                  "to": {
                      "ms_teams": {
                          "tenant_id": "your-tenant-id",
                          "service_url": "https://smba.trafficmanager.net/amer",
                          "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                          "channel_name": "general",
                      },
                  },
              },
          )
          ```

          ```bash cURL wrap theme={null}
          curl -X POST https://api.courier.com/send \
            -H "Authorization: Bearer $COURIER_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{
              "message": {
                "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
                "to": {
                  "ms_teams": {
                    "tenant_id": "your-tenant-id",
                    "service_url": "https://smba.trafficmanager.net/amer",
                    "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                    "channel_name": "general"
                  }
                }
              }
            }'
          ```

          ```ruby Ruby theme={null}
          response = courier.send_.message(
            message: {
              template: "nt_01kx4h2jdafq8bk9aftxak4b40",
              to: {
                ms_teams: {
                  tenant_id: "your-tenant-id",
                  service_url: "https://smba.trafficmanager.net/amer",
                  team_id: "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                  channel_name: "general"
                }
              }
            }
          )
          ```

          ```go Go theme={null}
          // This provider addresses the recipient with fields outside the typed
          // UserRecipient model, so pass the recipient as raw JSON.
          to := param.Override[shared.UserRecipientParam](json.RawMessage(`{
            "ms_teams": {
              "tenant_id": "your-tenant-id",
              "service_url": "https://smba.trafficmanager.net/amer",
              "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
              "channel_name": "general"
            }
          }`))

          response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
          	Message: courier.SendMessageParamsMessage{
          		To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
          		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
          	},
          })
          ```

          ```java Java theme={null}
          SendMessageParams params = SendMessageParams.builder()
              .message(SendMessageParams.Message.builder()
                  // This provider addresses the recipient with fields outside the
                  // typed UserRecipient model, so pass them as additional properties.
                  .to(UserRecipient.builder()
                      .putAdditionalProperty("ms_teams", JsonValue.from(java.util.Map.of(
                          "tenant_id", "your-tenant-id",
                          "service_url", "https://smba.trafficmanager.net/amer",
                          "team_id", "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                          "channel_name", "general"
                      )))
                      .build())
                  .template("nt_01kx4h2jdafq8bk9aftxak4b40")
                  .build())
              .build();
          SendMessageResponse response = client.send().message(params);
          ```

          ```php PHP theme={null}
          $response = $client->send->message(
            message: [
              'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
              'to' => [
                'ms_teams' => [
                  'tenant_id' => 'your-tenant-id',
                  'service_url' => 'https://smba.trafficmanager.net/amer',
                  'team_id' => 'feb55fc1-9e00-40f3-93b8-f7d14703f4dd',
                  'channel_name' => 'general',
                ],
              ],
            ],
          );
          ```

          ```csharp C# theme={null}
          SendMessageParams parameters = new()
          {
              Message = new()
              {
                  // This provider addresses the recipient with fields outside the
                  // typed UserRecipient model, so build it from raw JSON.
                  To = UserRecipient.FromRawUnchecked(
                      JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                          """
                          {
                            "ms_teams": {
                              "tenant_id": "your-tenant-id",
                              "service_url": "https://smba.trafficmanager.net/amer",
                              "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                              "channel_name": "general"
                            }
                          }
                          """
                      )
                  ),
                  Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              },
          };

          var response = await client.Send.Message(parameters);
          ```

          ```bash CLI wrap theme={null}
          courier send message \
            --api-key "$COURIER_API_KEY" \
            --message.to '{"ms_teams": {"tenant_id": "your-tenant-id", "service_url": "https://smba.trafficmanager.net/amer", "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd", "channel_name": "general"}}' \
            --message.template nt_01kx4h2jdafq8bk9aftxak4b40
          ```

          ```text MCP theme={null}
          With Courier MCP, send my template to the general channel in my Teams team.
          ```
        </CodeGroup>
      </Step>

      <Step title="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:

        ```json theme={null}
        {
          "reference": {
            "activityId": "1756945821955",
            "conversationId": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"
          }
        }
        ```
      </Step>

      <Step title="Send the thread reply">
        <CodeGroup>
          ```javascript Node.js theme={null}
          const { requestId } = await courier.send.message({
            message: {
              template: "nt_01kx4h2jdafq8bk9aftxak4b40",
              to: {
                ms_teams: {
                  tenant_id: "your-tenant-id",
                  service_url: "https://smba.trafficmanager.net/amer",
                  reply_to_activity_id: "1756945821955",
                  conversation_id: "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
                },
              },
            },
          });
          ```

          ```python Python theme={null}
          response = client.send.message(
              message={
                  "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
                  "to": {
                      "ms_teams": {
                          "tenant_id": "your-tenant-id",
                          "service_url": "https://smba.trafficmanager.net/amer",
                          "reply_to_activity_id": "1756945821955",
                          "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
                      },
                  },
              },
          )
          ```

          ```bash cURL wrap theme={null}
          curl -X POST https://api.courier.com/send \
            -H "Authorization: Bearer $COURIER_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{
              "message": {
                "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
                "to": {
                  "ms_teams": {
                    "tenant_id": "your-tenant-id",
                    "service_url": "https://smba.trafficmanager.net/amer",
                    "reply_to_activity_id": "1756945821955",
                    "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"
                  }
                }
              }
            }'
          ```

          ```ruby Ruby theme={null}
          response = courier.send_.message(
            message: {
              template: "nt_01kx4h2jdafq8bk9aftxak4b40",
              to: {
                ms_teams: {
                  tenant_id: "your-tenant-id",
                  service_url: "https://smba.trafficmanager.net/amer",
                  reply_to_activity_id: "1756945821955",
                  conversation_id: "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"
                }
              }
            }
          )
          ```

          ```go Go theme={null}
          // This provider addresses the recipient with fields outside the typed
          // UserRecipient model, so pass the recipient as raw JSON.
          to := param.Override[shared.UserRecipientParam](json.RawMessage(`{
            "ms_teams": {
              "tenant_id": "your-tenant-id",
              "service_url": "https://smba.trafficmanager.net/amer",
              "reply_to_activity_id": "1756945821955",
              "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"
            }
          }`))

          response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
          	Message: courier.SendMessageParamsMessage{
          		To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
          		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
          	},
          })
          ```

          ```java Java theme={null}
          SendMessageParams params = SendMessageParams.builder()
              .message(SendMessageParams.Message.builder()
                  // This provider addresses the recipient with fields outside the
                  // typed UserRecipient model, so pass them as additional properties.
                  .to(UserRecipient.builder()
                      .putAdditionalProperty("ms_teams", JsonValue.from(java.util.Map.of(
                          "tenant_id", "your-tenant-id",
                          "service_url", "https://smba.trafficmanager.net/amer",
                          "reply_to_activity_id", "1756945821955",
                          "conversation_id", "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"
                      )))
                      .build())
                  .template("nt_01kx4h2jdafq8bk9aftxak4b40")
                  .build())
              .build();
          SendMessageResponse response = client.send().message(params);
          ```

          ```php PHP theme={null}
          $response = $client->send->message(
            message: [
              'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
              'to' => [
                'ms_teams' => [
                  'tenant_id' => 'your-tenant-id',
                  'service_url' => 'https://smba.trafficmanager.net/amer',
                  'reply_to_activity_id' => '1756945821955',
                  'conversation_id' => '19:a52e21710de34c65b9f2e09ededaad2a@thread.skype',
                ],
              ],
            ],
          );
          ```

          ```csharp C# theme={null}
          SendMessageParams parameters = new()
          {
              Message = new()
              {
                  // This provider addresses the recipient with fields outside the
                  // typed UserRecipient model, so build it from raw JSON.
                  To = UserRecipient.FromRawUnchecked(
                      JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                          """
                          {
                            "ms_teams": {
                              "tenant_id": "your-tenant-id",
                              "service_url": "https://smba.trafficmanager.net/amer",
                              "reply_to_activity_id": "1756945821955",
                              "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"
                            }
                          }
                          """
                      )
                  ),
                  Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              },
          };

          var response = await client.Send.Message(parameters);
          ```

          ```bash CLI wrap theme={null}
          courier send message \
            --api-key "$COURIER_API_KEY" \
            --message.to '{"ms_teams": {"tenant_id": "your-tenant-id", "service_url": "https://smba.trafficmanager.net/amer", "reply_to_activity_id": "1756945821955", "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"}}' \
            --message.template nt_01kx4h2jdafq8bk9aftxak4b40
          ```

          ```text MCP theme={null}
          With Courier MCP, reply in that Teams thread using the activity id from the first message.
          ```
        </CodeGroup>
      </Step>
    </Steps>
  </Accordion>

  <Accordion title="Thread reply configuration">
    **Getting reference data**

    With webhooks configured, Courier delivers the reference data on every send:

    ```json theme={null}
    {
      "type": "message:updated",
      "data": {
        "id": "1-61f9dd53-b5c6969eb23c4aad6fce2ef7",
        "status": "SENT",
        "providers": {
          "msteams": {
            "reference": {
              "activityId": "1756945821955",
              "conversationId": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"
            }
          }
        }
      }
    }
    ```

    **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 handling**

    Common 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`.
  </Accordion>
</AccordionGroup>

## 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.

<AccordionGroup>
  <Accordion title="Understanding the relationship">
    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.
  </Accordion>

  <Accordion title="Sending an initial message">
    <CodeGroup>
      ```javascript Node.js theme={null}
      const { requestId } = await courier.send.message({
        message: {
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          to: {
            ms_teams: {
              team_id: "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
              channel_name: "General",
              tenant_id: "dabd1935-56a4-4305-938e-0840e2e84515",
              service_url: "https://smba.trafficmanager.net/amer",
            },
          },
        },
      });
      ```

      ```python Python theme={null}
      response = client.send.message(
          message={
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "to": {
                  "ms_teams": {
                      "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                      "channel_name": "General",
                      "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
                      "service_url": "https://smba.trafficmanager.net/amer",
                  },
              },
          },
      )
      ```

      ```bash cURL wrap theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "to": {
              "ms_teams": {
                "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                "channel_name": "General",
                "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
                "service_url": "https://smba.trafficmanager.net/amer"
              }
            }
          }
        }'
      ```

      ```ruby Ruby theme={null}
      response = courier.send_.message(
        message: {
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          to: {
            ms_teams: {
              team_id: "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
              channel_name: "General",
              tenant_id: "dabd1935-56a4-4305-938e-0840e2e84515",
              service_url: "https://smba.trafficmanager.net/amer"
            }
          }
        }
      )
      ```

      ```go Go theme={null}
      // This provider addresses the recipient with fields outside the typed
      // UserRecipient model, so pass the recipient as raw JSON.
      to := param.Override[shared.UserRecipientParam](json.RawMessage(`{
        "ms_teams": {
          "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
          "channel_name": "General",
          "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
          "service_url": "https://smba.trafficmanager.net/amer"
        }
      }`))

      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      	},
      })
      ```

      ```java Java theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              // This provider addresses the recipient with fields outside the
              // typed UserRecipient model, so pass them as additional properties.
              .to(UserRecipient.builder()
                  .putAdditionalProperty("ms_teams", JsonValue.from(java.util.Map.of(
                      "team_id", "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                      "channel_name", "General",
                      "tenant_id", "dabd1935-56a4-4305-938e-0840e2e84515",
                      "service_url", "https://smba.trafficmanager.net/amer"
                  )))
                  .build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .build())
          .build();
      SendMessageResponse response = client.send().message(params);
      ```

      ```php PHP theme={null}
      $response = $client->send->message(
        message: [
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'to' => [
            'ms_teams' => [
              'team_id' => 'feb55fc1-9e00-40f3-93b8-f7d14703f4dd',
              'channel_name' => 'General',
              'tenant_id' => 'dabd1935-56a4-4305-938e-0840e2e84515',
              'service_url' => 'https://smba.trafficmanager.net/amer',
            ],
          ],
        ],
      );
      ```

      ```csharp C# theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              // This provider addresses the recipient with fields outside the
              // typed UserRecipient model, so build it from raw JSON.
              To = UserRecipient.FromRawUnchecked(
                  JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                      """
                      {
                        "ms_teams": {
                          "team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd",
                          "channel_name": "General",
                          "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
                          "service_url": "https://smba.trafficmanager.net/amer"
                        }
                      }
                      """
                  )
              ),
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      };

      var response = await client.Send.Message(parameters);
      ```

      ```bash CLI wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"ms_teams": {"team_id": "feb55fc1-9e00-40f3-93b8-f7d14703f4dd", "channel_name": "General", "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515", "service_url": "https://smba.trafficmanager.net/amer"}}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40
      ```

      ```text MCP theme={null}
      With Courier MCP, send my template to the general channel in my Teams team and return the conversation id.
      ```
    </CodeGroup>

    The delivered event webhook contains:

    ```json theme={null}
    {
      "id": "1759342992794",
      "conversation_context": {
        "activityId": "1759342992794",
        "conversationId": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"
      }
    }
    ```

    The `conversationId` value `19:a52e21710de34c65b9f2e09ededaad2a@thread.skype` is the channel ID for the "General" channel.
  </Accordion>
</AccordionGroup>

### Replying in a thread

To reply to this message, use both the `activityId` and `conversationId` from the webhook:

<CodeGroup>
  ```javascript Node.js highlight={6} theme={null}
  const { requestId } = await courier.send.message({
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        ms_teams: {
          reply_to_activity_id: "1759342992794",
          conversation_id: "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
          tenant_id: "dabd1935-56a4-4305-938e-0840e2e84515",
          service_url: "https://smba.trafficmanager.net/amer",
        },
      },
    },
  });
  ```

  ```python Python highlight={6} theme={null}
  response = client.send.message(
      message={
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "to": {
              "ms_teams": {
                  "reply_to_activity_id": "1759342992794",
                  "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
                  "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
                  "service_url": "https://smba.trafficmanager.net/amer",
              },
          },
      },
  )
  ```

  ```bash cURL highlight={9} wrap theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "message": {
        "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
        "to": {
          "ms_teams": {
            "reply_to_activity_id": "1759342992794",
            "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
            "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
            "service_url": "https://smba.trafficmanager.net/amer"
          }
        }
      }
    }'
  ```

  ```ruby Ruby highlight={6} theme={null}
  response = courier.send_.message(
    message: {
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      to: {
        ms_teams: {
          reply_to_activity_id: "1759342992794",
          conversation_id: "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
          tenant_id: "dabd1935-56a4-4305-938e-0840e2e84515",
          service_url: "https://smba.trafficmanager.net/amer"
        }
      }
    }
  )
  ```

  ```go Go highlight={5} theme={null}
  // This provider addresses the recipient with fields outside the typed
  // UserRecipient model, so pass the recipient as raw JSON.
  to := param.Override[shared.UserRecipientParam](json.RawMessage(`{
    "ms_teams": {
      "reply_to_activity_id": "1759342992794",
      "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
      "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
      "service_url": "https://smba.trafficmanager.net/amer"
    }
  }`))

  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
  		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
  	},
  })
  ```

  ```java Java highlight={7} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          // This provider addresses the recipient with fields outside the
          // typed UserRecipient model, so pass them as additional properties.
          .to(UserRecipient.builder()
              .putAdditionalProperty("ms_teams", JsonValue.from(java.util.Map.of(
                  "reply_to_activity_id", "1759342992794",
                  "conversation_id", "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
                  "tenant_id", "dabd1935-56a4-4305-938e-0840e2e84515",
                  "service_url", "https://smba.trafficmanager.net/amer"
              )))
              .build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={6} theme={null}
  $response = $client->send->message(
    message: [
      'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
      'to' => [
        'ms_teams' => [
          'reply_to_activity_id' => '1759342992794',
          'conversation_id' => '19:a52e21710de34c65b9f2e09ededaad2a@thread.skype',
          'tenant_id' => 'dabd1935-56a4-4305-938e-0840e2e84515',
          'service_url' => 'https://smba.trafficmanager.net/amer',
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={12} theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          // This provider addresses the recipient with fields outside the
          // typed UserRecipient model, so build it from raw JSON.
          To = UserRecipient.FromRawUnchecked(
              JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                  """
                  {
                    "ms_teams": {
                      "reply_to_activity_id": "1759342992794",
                      "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
                      "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
                      "service_url": "https://smba.trafficmanager.net/amer"
                    }
                  }
                  """
              )
          ),
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={3} wrap theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"ms_teams": {"reply_to_activity_id": "1759342992794", "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype", "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515", "service_url": "https://smba.trafficmanager.net/amer"}}' \
    --message.template nt_01kx4h2jdafq8bk9aftxak4b40
  ```

  ```text MCP theme={null}
  With Courier MCP, reply to that Teams message using its activity id and conversation id.
  ```
</CodeGroup>

The reply's delivered event maintains the same `conversationId`:

```json theme={null}
{
  "id": "1759343317452",
  "conversation_context": {
    "activityId": "1759343317452",
    "conversationId": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype"
  }
}
```

The `conversationId` stays the same across a thread because it represents the channel. Only the `activityId` changes with each message.

<AccordionGroup>
  <Accordion title="Getting channel IDs directly from Teams">
    You can get a channel ID without sending a message first. In the [Teams web app](https://teams.microsoft.com/v2/), select the channel and click the ellipsis (⋯) in the upper right corner. Choose "Get link to channel" to copy a URL like:

    ```
    https://teams.microsoft.com/l/channel/19%3Aa52e21710de34c65b9f2e09ededaad2a%40thread.skype/General?groupId=feb55fc1-9e00-40f3-93b8-f7d14703f4dd&tenantId=dabd1935-56a4-4305-938e-0840e2e84515
    ```

    The channel ID appears after `/channel/` in URL-encoded format. Decode `19%3Aa52e21710de34c65b9f2e09ededaad2a%40thread.skype` to get `19:a52e21710de34c65b9f2e09ededaad2a@thread.skype`.
  </Accordion>

  <Accordion title="Sending directly with conversation_id">
    When you know the channel ID, send with `conversation_id` instead of looking up the channel by name:

    <CodeGroup>
      ```javascript Node.js highlight={6} theme={null}
      const { requestId } = await courier.send.message({
        message: {
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          to: {
            ms_teams: {
              conversation_id: "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
              tenant_id: "dabd1935-56a4-4305-938e-0840e2e84515",
              service_url: "https://smba.trafficmanager.net/amer",
            },
          },
        },
      });
      ```

      ```python Python highlight={6} theme={null}
      response = client.send.message(
          message={
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "to": {
                  "ms_teams": {
                      "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
                      "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
                      "service_url": "https://smba.trafficmanager.net/amer",
                  },
              },
          },
      )
      ```

      ```bash cURL highlight={9} wrap theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "to": {
              "ms_teams": {
                "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
                "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
                "service_url": "https://smba.trafficmanager.net/amer"
              }
            }
          }
        }'
      ```

      ```ruby Ruby highlight={6} theme={null}
      response = courier.send_.message(
        message: {
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          to: {
            ms_teams: {
              conversation_id: "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
              tenant_id: "dabd1935-56a4-4305-938e-0840e2e84515",
              service_url: "https://smba.trafficmanager.net/amer"
            }
          }
        }
      )
      ```

      ```go Go highlight={5} theme={null}
      // This provider addresses the recipient with fields outside the typed
      // UserRecipient model, so pass the recipient as raw JSON.
      to := param.Override[shared.UserRecipientParam](json.RawMessage(`{
        "ms_teams": {
          "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
          "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
          "service_url": "https://smba.trafficmanager.net/amer"
        }
      }`))

      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      	},
      })
      ```

      ```java Java highlight={7} theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              // This provider addresses the recipient with fields outside the
              // typed UserRecipient model, so pass them as additional properties.
              .to(UserRecipient.builder()
                  .putAdditionalProperty("ms_teams", JsonValue.from(java.util.Map.of(
                      "conversation_id", "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
                      "tenant_id", "dabd1935-56a4-4305-938e-0840e2e84515",
                      "service_url", "https://smba.trafficmanager.net/amer"
                  )))
                  .build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .build())
          .build();
      SendMessageResponse response = client.send().message(params);
      ```

      ```php PHP highlight={6} theme={null}
      $response = $client->send->message(
        message: [
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'to' => [
            'ms_teams' => [
              'conversation_id' => '19:a52e21710de34c65b9f2e09ededaad2a@thread.skype',
              'tenant_id' => 'dabd1935-56a4-4305-938e-0840e2e84515',
              'service_url' => 'https://smba.trafficmanager.net/amer',
            ],
          ],
        ],
      );
      ```

      ```csharp C# highlight={12} theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              // This provider addresses the recipient with fields outside the
              // typed UserRecipient model, so build it from raw JSON.
              To = UserRecipient.FromRawUnchecked(
                  JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
                      """
                      {
                        "ms_teams": {
                          "conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype",
                          "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515",
                          "service_url": "https://smba.trafficmanager.net/amer"
                        }
                      }
                      """
                  )
              ),
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          },
      };

      var response = await client.Send.Message(parameters);
      ```

      ```bash CLI highlight={3} wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"ms_teams": {"conversation_id": "19:a52e21710de34c65b9f2e09ededaad2a@thread.skype", "tenant_id": "dabd1935-56a4-4305-938e-0840e2e84515", "service_url": "https://smba.trafficmanager.net/amer"}}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40
      ```

      ```text MCP theme={null}
      With Courier MCP, send my template straight to this Teams conversation id.
      ```
    </CodeGroup>

    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.
  </Accordion>

  <Accordion title="Technical details">
    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.
  </Accordion>
</AccordionGroup>

## Overrides

<Doc href="/docs/send/overrides#how-overrides-work">How overrides work</Doc> covers the two levels and which one wins.

A `config` override swaps the Azure Bot's App ID and App Password for one send:

<CodeGroup>
  ```javascript Node.js highlight={8-15} theme={null}
  const { requestId } = await courier.send.message({
    message: {
      to: {
        user_id: "user_123",
      },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      providers: {
        msteams: {
          override: {
            config: {
              appId: "YOUR_APP_ID",
              appPassword: "YOUR_APP_PASSWORD",
            },
          },
        },
      },
    },
  });
  ```

  ```python Python highlight={8-15} theme={null}
  response = client.send.message(
      message={
          "to": {
            "user_id": "user_123",
          },
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "providers": {
              "msteams": {
                  "override": {
                    "config": {
                      "appId": "YOUR_APP_ID",
                      "appPassword": "YOUR_APP_PASSWORD",
                    },
                  },
              },
          },
      },
  )
  ```

  ```bash cURL highlight={11-18} wrap theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
        "message": {
          "to": {
            "user_id": "user_123"
          },
          "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
          "providers": {
            "msteams": {
              "override": {
                "config": {
                  "appId": "YOUR_APP_ID",
                  "appPassword": "YOUR_APP_PASSWORD"
                }
              }
            }
          }
        }
      }'
  ```

  ```ruby Ruby highlight={8-15} theme={null}
  response = courier.send_.message(
    message: {
      to: {
        user_id: "user_123"
      },
      template: "nt_01kx4h2jdafq8bk9aftxak4b40",
      providers: {
        "msteams" => {
          override: {
            config: {
              appId: "YOUR_APP_ID",
              appPassword: "YOUR_APP_PASSWORD"
            }
          }
        }
      }
    }
  )
  ```

  ```go Go highlight={10-17} theme={null}
  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{
  			OfUserRecipient: &shared.UserRecipientParam{
  				UserID: courier.String("user_123"),
  			},
  		},
  		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
  		Providers: shared.MessageProvidersParam{
  			"msteams": shared.MessageProvidersTypeParam{
  				Override: map[string]any{
  					"config": map[string]any{
  						"appId": "YOUR_APP_ID",
  						"appPassword": "YOUR_APP_PASSWORD",
  					},
  				},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={6-7} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(UserRecipient.builder().userId("user_123").build())
          .template("nt_01kx4h2jdafq8bk9aftxak4b40")
          .providers(MessageProviders.builder()
              .putAdditionalProperty("msteams", JsonValue.from(java.util.Map.of(
                  "override", java.util.Map.of("config", java.util.Map.of("appId", "YOUR_APP_ID", "appPassword", "YOUR_APP_PASSWORD")))))
              .build())
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={8-15} theme={null}
  $response = $client->send->message(
    message: [
      'to' => [
        'user_id' => 'user_123',
      ],
      'template' => "nt_01kx4h2jdafq8bk9aftxak4b40",
      'providers' => [
        'msteams' => [
          'override' => [
            'config' => [
              'appId' => 'YOUR_APP_ID',
              'appPassword' => 'YOUR_APP_PASSWORD',
            ],
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={9-18} theme={null}
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { UserID = "user_123" },
          Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
          Providers = new Dictionary<string, MessageProvidersType>()
          {
              {
                  "msteams",
                  new()
                  {
                      Override = new Dictionary<string, JsonElement>()
                      {
                          { "config", JsonSerializer.SerializeToElement(new { appId = "YOUR_APP_ID", appPassword = "YOUR_APP_PASSWORD" }) },
                      },
                  }
              },
          },
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={5} wrap theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"user_id": "user_123"}' \
    --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
    --message.providers '{"msteams": {"override": {"config": {"appId": "YOUR_APP_ID", "appPassword": "YOUR_APP_PASSWORD"}}}}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send my template to user_123 on Microsoft Teams using a different Azure bot app id.
  ```
</CodeGroup>

## Adaptive Cards

Courier supports Microsoft Teams [Adaptive Cards](https://adaptivecards.io/) through Jsonnet blocks in Design Studio.

<AccordionGroup>
  <Accordion title="Using Jsonnet blocks">
    Jsonnet blocks let you customize Adaptive Cards sent through Microsoft Teams. To send your first card:

    <Steps>
      <Step title="Add a Microsoft Teams channel">
        In Design Studio, add a new Microsoft Teams channel.
      </Step>

      <Step title="Insert a Jsonnet block">
        Insert a Jsonnet block within your message.

        <Frame caption="Jsonnet Block">
          <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-designer-jsonnet-block.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=ec51f7740ecd6317b5c0e518290f9e54" width="1599" height="743" data-path="assets/teams-designer-jsonnet-block.webp" />
        </Frame>
      </Step>

      <Step title="Open the Adaptive Cards Designer">
        Open the [Adaptive Cards Designer](https://adaptivecards.io/designer/) and select or create a card layout.
      </Step>

      <Step title="Copy the card JSON into your Jsonnet block">
        Copy the card JSON from the "Card Payload Editor" into your Courier Jsonnet block.

        <Frame caption="Sample Jsonnet">
          <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-designer-jsonnet-sample.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=2b00895da20345c3fdf1bf7bb6a47eab" width="1584" height="1002" data-path="assets/teams-designer-jsonnet-sample.webp" />
        </Frame>
      </Step>
    </Steps>
  </Accordion>

  <Accordion title="Sending an Adaptive Card">
    Include the sample data from the Adaptive Cards Designer in your Courier message:

    <CodeGroup>
      ```javascript Node.js theme={null}
      const { requestId } = await courier.send.message({
        message: {
          to: {
            user_id: "user_123",
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {
            title: "Publish Adaptive Card Schema",
            description: "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.",
            creator: {
              name: "Sarah Bennett",
              profileImage: "https://www.acme-corp.com/avatar.png",
            },
            createdUtc: "2017-02-14T06:08:39Z",
            viewUrl: "https://adaptivecards.io",
            properties: [
              {
                key: "Board",
                value: "Adaptive Cards",
              },
              {
                key: "List",
                value: "Backlog",
              },
              {
                key: "Assigned to",
                value: "Sarah Bennett",
              },
              {
                key: "Due date",
                value: "Not set",
              },
            ],
          },
        },
      });
      ```

      ```python Python theme={null}
      response = client.send.message(
          message={
              "to": {
                  "user_id": "user_123",
              },
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "data": {
                  "title": "Publish Adaptive Card Schema",
                  "description": "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.",
                  "creator": {
                      "name": "Sarah Bennett",
                      "profileImage": "https://www.acme-corp.com/avatar.png",
                  },
                  "createdUtc": "2017-02-14T06:08:39Z",
                  "viewUrl": "https://adaptivecards.io",
                  "properties": [
                      {
                          "key": "Board",
                          "value": "Adaptive Cards",
                      },
                      {
                          "key": "List",
                          "value": "Backlog",
                      },
                      {
                          "key": "Assigned to",
                          "value": "Sarah Bennett",
                      },
                      {
                          "key": "Due date",
                          "value": "Not set",
                      },
                  ],
              },
          },
      )
      ```

      ```bash cURL wrap theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "to": {
              "user_id": "user_123"
            },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "data": {
              "title": "Publish Adaptive Card Schema",
              "description": "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.",
              "creator": {
                "name": "Sarah Bennett",
                "profileImage": "https://www.acme-corp.com/avatar.png"
              },
              "createdUtc": "2017-02-14T06:08:39Z",
              "viewUrl": "https://adaptivecards.io",
              "properties": [
                {
                  "key": "Board",
                  "value": "Adaptive Cards"
                },
                {
                  "key": "List",
                  "value": "Backlog"
                },
                {
                  "key": "Assigned to",
                  "value": "Sarah Bennett"
                },
                {
                  "key": "Due date",
                  "value": "Not set"
                }
              ]
            }
          }
        }'
      ```

      ```ruby Ruby theme={null}
      response = courier.send_.message(
        message: {
          to: {
            user_id: "user_123"
          },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {
            title: "Publish Adaptive Card Schema",
            description: "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.",
            creator: {
              name: "Sarah Bennett",
              profileImage: "https://www.acme-corp.com/avatar.png"
            },
            createdUtc: "2017-02-14T06:08:39Z",
            viewUrl: "https://adaptivecards.io",
            properties: [
              {
                key: "Board",
                value: "Adaptive Cards"
              },
              {
                key: "List",
                value: "Backlog"
              },
              {
                key: "Assigned to",
                value: "Sarah Bennett"
              },
              {
                key: "Due date",
                value: "Not set"
              }
            ]
          }
        }
      )
      ```

      ```go Go theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{
      			OfUserRecipient: &shared.UserRecipientParam{
      				UserID: courier.String("user_123"),
      			},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      		Data: map[string]any{
      			"title": "Publish Adaptive Card Schema",
      			"description": "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.",
      			"creator": map[string]any{
      				"name": "Sarah Bennett",
      				"profileImage": "https://www.acme-corp.com/avatar.png",
      			},
      			"createdUtc": "2017-02-14T06:08:39Z",
      			"viewUrl": "https://adaptivecards.io",
      			"properties": []any{
      				map[string]any{
      					"key": "Board",
      					"value": "Adaptive Cards",
      				},
      				map[string]any{
      					"key": "List",
      					"value": "Backlog",
      				},
      				map[string]any{
      					"key": "Assigned to",
      					"value": "Sarah Bennett",
      				},
      				map[string]any{
      					"key": "Due date",
      					"value": "Not set",
      				},
      			},
      		},
      	},
      })
      ```

      ```java Java theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(UserRecipient.builder().userId("user_123").build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .data(JsonValue.from(java.util.Map.of(
                  "title", "Publish Adaptive Card Schema",
                  "description", "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.",
                  "creator", java.util.Map.of(
                      "name", "Sarah Bennett",
                      "profileImage", "https://www.acme-corp.com/avatar.png"
                  ),
                  "createdUtc", "2017-02-14T06:08:39Z",
                  "viewUrl", "https://adaptivecards.io",
                  "properties", java.util.List.of(
                      java.util.Map.of(
                          "key", "Board",
                          "value", "Adaptive Cards"
                      ),
                      java.util.Map.of(
                          "key", "List",
                          "value", "Backlog"
                      ),
                      java.util.Map.of(
                          "key", "Assigned to",
                          "value", "Sarah Bennett"
                      ),
                      java.util.Map.of(
                          "key", "Due date",
                          "value", "Not set"
                      )
                  )
              )))
              .build())
          .build();
      SendMessageResponse response = client.send().message(params);
      ```

      ```php PHP theme={null}
      $response = $client->send->message(
        message: [
          'to' => [
            'user_id' => 'user_123',
          ],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'data' => [
            'title' => 'Publish Adaptive Card Schema',
            'description' => 'Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.',
            'creator' => [
              'name' => 'Sarah Bennett',
              'profileImage' => 'https://www.acme-corp.com/avatar.png',
            ],
            'createdUtc' => '2017-02-14T06:08:39Z',
            'viewUrl' => 'https://adaptivecards.io',
            'properties' => [
              [
                'key' => 'Board',
                'value' => 'Adaptive Cards',
              ],
              [
                'key' => 'List',
                'value' => 'Backlog',
              ],
              [
                'key' => 'Assigned to',
                'value' => 'Sarah Bennett',
              ],
              [
                'key' => 'Due date',
                'value' => 'Not set',
              ],
            ],
          ],
        ],
      );
      ```

      ```csharp C# theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { UserID = "user_123" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              Data = new Dictionary<string, JsonElement>()
              {
                  { "title", JsonSerializer.SerializeToElement("Publish Adaptive Card Schema") },
                  { "description", JsonSerializer.SerializeToElement("Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.") },
                  { "creator", JsonSerializer.Deserialize<JsonElement>(
                      """
                      {
                        "name": "Sarah Bennett",
                        "profileImage": "https://www.acme-corp.com/avatar.png"
                      }
                      """
                  ) },
                  { "createdUtc", JsonSerializer.SerializeToElement("2017-02-14T06:08:39Z") },
                  { "viewUrl", JsonSerializer.SerializeToElement("https://adaptivecards.io") },
                  { "properties", JsonSerializer.Deserialize<JsonElement>(
                      """
                      [
                        {
                          "key": "Board",
                          "value": "Adaptive Cards"
                        },
                        {
                          "key": "List",
                          "value": "Backlog"
                        },
                        {
                          "key": "Assigned to",
                          "value": "Sarah Bennett"
                        },
                        {
                          "key": "Due date",
                          "value": "Not set"
                        }
                      ]
                      """
                  ) },
              },
          },
      };

      var response = await client.Send.Message(parameters);
      ```

      ```bash CLI wrap theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"user_id": "user_123"}' \
        --message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
        --message.data '{"title": "Publish Adaptive Card Schema", "description": "Now that we have defined the main rules and features of the format, we need to produce a schema and publish it to GitHub. The schema will be the starting point of our reference documentation.", "creator": {"name": "Sarah Bennett", "profileImage": "https://www.acme-corp.com/avatar.png"}, "createdUtc": "2017-02-14T06:08:39Z", "viewUrl": "https://adaptivecards.io", "properties": [{"key": "Board", "value": "Adaptive Cards"}, {"key": "List", "value": "Backlog"}, {"key": "Assigned to", "value": "Sarah Bennett"}, {"key": "Due date", "value": "Not set"}]}'
      ```

      ```text MCP theme={null}
      With Courier MCP, send my template to user user_123 as a Teams Adaptive Card.
      ```
    </CodeGroup>

    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:

    <Frame caption="MS Teams Adaptive Card">
      <img src="https://mintcdn.com/courier-4f1f25dc/rYENcCCTyPPrDtw0/assets/teams-adaptive-card.webp?fit=max&auto=format&n=rYENcCCTyPPrDtw0&q=85&s=4966131a12a67d84ddddf3aa0ac1c136" width="1033" height="417" data-path="assets/teams-adaptive-card.webp" />
    </Frame>
  </Accordion>

  <Accordion title="Using @mentions in Adaptive Cards">
    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:

    ```json theme={null}
    {
      "msteams": {
        "entities": [
          {
            "type": "mention",
            "text": "<at>Sarah Bennett</at>",
            "mentioned": {
              "id": "29:123124124124",
              "name": "Sarah Bennett"
            }
          }
        ]
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## 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

```mermaid theme={null}
flowchart LR
    A["Courier"] -->|Bot ID| B["Azure bot"]
    B -->|Endpoint| C["Your bot service"]
    C -->|App ID| D["Teams app"]
    D --> E["Channel or user"]
```

## Provider details

```text theme={null}
msteams
```

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

<Card title="Send to a specific provider" icon="bullseye-arrow" href="/docs/send/send-to-a-provider" horizontal arrow="true">
  When that is worth doing, and what you give up: failover, channel priority, and providers you add later.
</Card>
