Microsoft Teams

Deploy bots to Microsoft Teams with Adaptive Cards, mentions, and conversation threading.

Create a new bot

Scaffold a Next.js bot with the Microsoft Teams adapter using create-chat-sdk:

pnpm create chat-sdk@latest my-bot --adapter teams memory -y

The generated app uses the Memory state adapter for local development. To add the adapter to an existing app, start at Install.

Install

pnpm add @chat-adapter/teams @chat-adapter/state-memory

Quick start

The adapter auto-detects TEAMS_APP_ID, TEAMS_APP_PASSWORD, and TEAMS_APP_TENANT_ID from the environment.

For managed credentials and webhook verification, see Vercel Connect.

lib/bot.ts
import { Chat } from "chat";
import { createTeamsAdapter } from "@chat-adapter/teams";
import { createMemoryState } from "@chat-adapter/state-memory";

export const bot = new Chat({
  userName: "mybot",
  adapters: {
    teams: createTeamsAdapter({
      appType: "SingleTenant",
    }),
  },
  state: createMemoryState(),
});

bot.onNewMention(async (thread, message) => {
  await thread.post("Hello from Teams!");
});

The memory state adapter keeps subscriptions and locks in process memory, which suits local development. Use Redis or PostgreSQL in production.

Then create the webhook route:

app/api/webhooks/teams/route.ts
import { after } from "next/server";
import { bot } from "@/lib/bot";

export async function POST(request: Request): Promise<Response> {
  return bot.webhooks.teams(request, {
    waitUntil: (task) => after(() => task),
  });
}

Platform setup

The Teams CLI handles AAD app registration, client secret generation, bot registration, and Teams channel setup in one command. If you use Vercel Connect, you don't need to copy the generated app ID and password into your environment.

Install the CLI

npm install -g @microsoft/teams.cli

Create the app

teams login
teams status
teams app create --name "My Bot" --endpoint "https://your-domain.com/api/webhooks/teams" --env .env

For local development, use a tunnel such as devtunnel, or ngrok to expose your local server.

Credentials (CLIENT_ID, CLIENT_SECRET, TENANT_ID) are written to .env. Rename them to match the adapter:

TEAMS_APP_ID=<CLIENT_ID>
TEAMS_APP_PASSWORD=<CLIENT_SECRET>
TEAMS_APP_TENANT_ID=<TENANT_ID>

Install in Teams

Get a direct install link:

teams app get <appId> --install-link

Or download the app package for sideloading, then in Teams go to Apps > Manage your apps > Upload an app > Upload a custom app.

Verify

Check the registration, app health, manifest, and endpoint:

teams app doctor <appId>

Authentication

Use Vercel Connect to manage the bot identity and tokens, or configure Microsoft app credentials directly with a client secret, federated workload identity, or a custom token factory.

Vercel Connect

Use Vercel Connect to resolve the bot's app ID and request short-lived tokens for Bot Framework and Microsoft Graph:

import { createTeamsAdapter } from "@chat-adapter/teams";
import { connectTeamsAdapter } from "@vercel/connect/chat";

const teams = createTeamsAdapter({
  ...connectTeamsAdapter("microsoft-teams/acme-teams"),
});

The helper resolves appId during initialization and supplies a scope-aware token callback. It also verifies Connect-forwarded webhooks with Vercel OIDC instead of Microsoft's native JWT. Enable trigger forwarding to /api/webhooks/teams and omit TEAMS_APP_ID and TEAMS_APP_PASSWORD when using the helper.

The callback requests separate tokens for https://api.botframework.com/.default and https://graph.microsoft.com/.default. Graph reads still require the appropriate installation and consent permissions; Connect uses the bot's home tenant for managed tokens.

An appId resolver runs once during successful initialization. Call await bot.initialize() before using adapter methods directly; Chat initializes automatically before handling webhooks. botUserId becomes available after initialization when using a resolver.

A custom webhookVerifier receives the incoming request and its raw body. Return a truthy value to accept, or return a falsy value or throw to reject with HTTP 401. Without a verifier, the adapter retains Microsoft's native JWT verification.

Create the connector and attach it to your project with the Vercel CLI. --trigger-path registers the Teams webhook route as the trigger destination, and vercel env pull downloads a development OIDC token for local runs:

vercel link
vercel connect create microsoft-teams --name acme-teams --triggers
vercel connect attach microsoft-teams/acme-teams --triggers --trigger-path /api/webhooks/teams
vercel env pull

Without --trigger-branch, Connect forwards webhooks to your production deployment. See Vercel Connect for the full setup.

Client secret

Provide appPassword, or set TEAMS_APP_PASSWORD, with the client secret generated during platform setup:

createTeamsAdapter({
  appPassword: "your_app_password_here",
});

Federated (workload identity)

For environments with managed identities, such as AKS or GitHub Actions, use federated workload identity. federated.clientId maps to managedIdentityClientId in the Teams SDK:

createTeamsAdapter({
  federated: {
    clientId: "your_managed_identity_client_id_here",
  },
});

Custom token factory

For runtimes without access to Azure IMDS, such as serverless platforms, provide your own token-minting logic. token maps to AppOptions.token in the Teams SDK:

createTeamsAdapter({
  appId: "your_app_id_here",
  appTenantId: "your_tenant_id_here",
  token: async (scope, tenantId) => {
    // fetch or mint an access token for the given scope/tenant however your
    // runtime supports it (for example, a workload-identity federation bridge)
    return await getAccessToken(scope, tenantId);
  },
});

A custom token factory takes precedence over configured credentials and client-secret environment variables, including CLIENT_SECRET.

Configuration

Prop

Type

appId is required, along with one authentication method (appPassword, federated, or token). If more than one is configured, token takes precedence over federated, which takes precedence over appPassword.

Environment variables

VariableRequiredDescription
TEAMS_APP_IDYes, unless you pass appId or use Vercel ConnectAzure Bot App ID.
TEAMS_APP_PASSWORDWith client secret auth, unless you pass appPasswordAzure Bot App Password (client secret). Ignored when federated or token is set.
TEAMS_APP_TENANT_IDFor SingleTenant apps and message historyAzure AD Tenant ID.
TEAMS_API_URLNoTeams API base URL override, for example for GCC-High or sovereign clouds.

Mentions

Incoming Teams mentions are supported. Outgoing @name text, including multi-word names, stays plain text and does not notify the user. The adapter does not resolve display names into user identities or generate mention entities. Teams requires matching mention text and entities for a real mention; <at> markup alone is not sufficient.

Message history

Fetching message history requires TEAMS_APP_TENANT_ID and the right permissions depending on the conversation type:

ContextPermissionTypeAdmin consent?
ChannelChannelMessage.Read.GroupRSCNo
Group chatChatMessage.Read.ChatRSCNo
DMChat.Read.AllAzure ADYes

RSC permissions are set via the Teams CLI (no admin consent needed):

teams app rsc add <appId> ChannelMessage.Read.Group --type Application
teams app rsc add <appId> ChatMessage.Read.Chat --type Application

For DM message history, RSC is not sufficient. Add Chat.Read.All via the Azure CLI:

az ad app permission add \
  --id <appId> \
  --api 00000003-0000-0000-c000-000000000000 \
  --api-permissions 6b7d71aa-70aa-4810-a8d9-5d9fb2830017=Role

az ad app permission admin-consent --id <appId>

Receiving all messages

By default, Teams bots only receive messages when directly @-mentioned. The RSC permissions above also enable receiving all messages in channels and group chats as a side effect.

The adapter sets isBot for the adapter's own bot, senders with from.role: "bot", and Teams bot account IDs starting with 28:. isMe identifies only the adapter's own bot. The same values appear on message.author and on event.user for reactions, card actions, and dialog events, so handlers can exclude bot activity from human-only conversations.

Bot joins

onMemberJoinedChannel fires when a conversationUpdate activity adds this bot to a channel or group chat. adapter.botUserId is the Teams bot ID (28:<appId>), so the same bot-join guard works across adapters:

bot.onMemberJoinedChannel(async (event) => {
  if (event.userId !== event.adapter.botUserId) {
    return;
  }
  await bot.channel(event.channelId).post("Hello! Thanks for adding me.");
});

For team installations, channelId identifies the channel selected during installation, not the team ID. inviterId comes from the activity sender. Pass your platform's waitUntil to the webhook handler to track asynchronous welcome handlers.

This dispatches bot joins only. Personal installs, ordinary member additions, and removals do not emit this event; use onInstalled for install welcomes that should also cover personal scope. See Microsoft's conversation event documentation for the underlying payloads.

Installation lifecycle

bot.onInstalled fires for installationUpdate actions add and add-upgrade. bot.onUninstalled fires for remove and remove-upgrade. Both cover personal, group chat, and team installs and receive the action, the activity id, the platform conversationId, the actor userId, tenantId, locale, and the raw activity. These handlers are currently emitted only by Teams.

event.channelId is the Chat SDK destination for the installation: the personal chat, the group chat, or the channel selected during a team install. It encodes the conversation's Bot Framework service URL, so you can persist the string and post to it later from any process with bot.channel(channelId).post(). When the activity body carries no service URL, the adapter uses the one from the validated request token, so channelId is normally present.

Keep the installation key separate from the posting destination. For team installs, use event.raw.channelData.team.id: the add event can identify the selected channel while removal identifies the team/root conversation. Personal and group chat installs can use conversationId. Include the bot ID, tenant ID, and scope in the key so unrelated installations stay separate.

import type { TeamsActivity } from "@chat-adapter/teams/webhook";
import type { InstallationEvent } from "chat";

function installationKey(event: InstallationEvent) {
  const activity = event.raw as TeamsActivity;
  const teamId = activity.channelData?.team?.id;
  return JSON.stringify([
    event.adapter.botUserId,
    event.tenantId,
    teamId ? "team" : "conversation",
    teamId ?? event.conversationId,
  ]);
}

// `installationStore` is your application's durable key/value store.
bot.onInstalled(async (event) => {
  if (event.adapter.name !== "teams" || !event.channelId) return;
  const key = installationKey(event);
  // An upsert also replaces the destination after a reinstall or upgrade.
  await installationStore.set(key, event.channelId);

  await bot.channel(event.channelId).post("Thanks for installing!");
});

bot.onUninstalled(async (event) => {
  if (event.adapter.name !== "teams") return;
  const key = installationKey(event);
  await installationStore.delete(key);
});

A team or group chat install produces both a conversationUpdate that adds the bot and an installationUpdate, so onMemberJoinedChannel and onInstalled both fire for the same conversation. Send install welcomes from one of them. If you use onMemberJoinedChannel for Slack, skip Teams there by checking event.adapter.name. Personal installs emit onInstalled only.

add-upgrade means an app upgrade added the bot to the manifest; remove-upgrade means it removed the bot. Handle them as installation and removal, respectively, including persistence and cleanup. Routine upgrades that leave the bot in place do not emit installationUpdate.

Delivery may be retried, so make side effects idempotent, for example keyed on the installation key and activity ID. The SDK does not subscribe, persist, or deduplicate installations for you. Do not send a farewell after removal. Pass your host's waitUntil to the webhook handler so persistence and onboarding finish after the response.

If you already own routing and only need the connector call, decode the stored channelId with decodeThreadId and pass its conversationId and serviceUrl to postTeamsMessage from @chat-adapter/teams/api. Do not pass the installation activity ID as replyToId; it is not a message. Full team and selected-channel metadata remain in event.raw.

See Microsoft's installation update documentation.

Starting channel threads

A channel post starts a new Teams thread. Use the returned threadId to post follow-ups to that thread:

const root = await bot.channel(channelId).post("New support request");
await bot.thread(root.threadId).post("Follow-up for this request");

Both text and Adaptive Card posts return the created thread's ID. Group chats and personal conversations retain their existing conversation IDs.

Targeted / ephemeral messages

Teams targeted messages are available in public preview. Use thread.postEphemeral() or channel.postEphemeral() to send a native Teams message that only the selected conversation member can see:

await thread.postEphemeral(message.author, "Only you can see this.", {
  fallbackToDM: false,
});

The result has usedFallback: false when Teams accepts the targeted message.

AI-generated content labels

Set aiGenerated: true on a message object to show Teams' "AI generated" label on the bot's message. The label works on new, edited, channel, and targeted messages:

await thread.post({ markdown: answer, aiGenerated: true });

The flag works with markdown, raw, ast, and card messages. A plain string, a bare card element, or a JSX <Card> is sent without the label, so static messages such as help text and errors stay unlabeled. To label a card, pass it as { card, aiGenerated: true }, where card is a CardElement. Convert a JSX card with toCardElement() from chat first.

To label a streamed response, wrap the stream in a StreamingPlan:

import { StreamingPlan } from "chat";

await thread.post(
  new StreamingPlan(result.fullStream, { aiGenerated: true })
);

The label applies to native streaming in DMs, to the buffered message in group chats and channels, and to every edit after the placeholder when you configure post-and-edit fallback streaming. Passing a stream to thread.post() without a StreamingPlan posts it unlabeled.

An edit replaces the whole Teams activity, including its entities. Pass aiGenerated: true again on every edit, or the label is removed:

const sent = await thread.post({ markdown: draft, aiGenerated: true });
await sent.edit({ markdown: finalAnswer, aiGenerated: true });

Messages posted through the built-in AI tools are labeled when the model sends a markdown or raw body. A plain-string body is sent unlabeled.

See Microsoft's AI content labels documentation.

User lookup

bot.getUser() looks up user profiles through the Microsoft Graph API. To enable it:

  1. Grant the User.Read.All application permission in your Azure AD app registration.
  2. Grant admin consent for the permission.
const user = await bot.getUser(message.author);
console.log(user?.email);    // "alice@contoso.com"
console.log(user?.fullName); // "Alice Smith"

The adapter caches each user's Azure AD object ID from incoming activities, so getUser only works for users who have previously interacted with the bot.

Live incoming message authors include email when the Teams conversation members API resolves the sender. This lookup uses the bot's access to the current conversation and does not require Microsoft Graph permissions. Lookup failures leave message.author.email undefined without preventing message delivery. Edited-message events and messages returned by fetchMessages are not hydrated with an email.

Incoming attachments

Incoming inline images and files are exposed through message.attachments with a lazy fetchData() method. The adapter authenticates connector-hosted inline attachments through the configured Teams bot client, while Teams file download cards use their direct download URL without the bot token.

Anonymous downloads require HTTPS URLs that resolve to public addresses; private and internal hosts are refused, including after redirects. Responses are limited to 25 MB and downloads time out after 30 seconds. Inline attachments served by the connector use bot authentication instead, including the Bot Framework Emulator's plain-HTTP loopback connector. To change the limits or route downloads through a proxy, override createFetchDataFn() in a subclass and call downloadAttachment from @chat-adapter/shared with your own options.

Low-level APIs

Use the low-level Teams subpaths when your app already owns routing, state, sessions, or workflow execution and only needs Teams-specific primitives.

SubpathUse for
@chat-adapter/teams/webhookParse Bot Framework Activity JSON, classify common payloads, and extract continuation data
@chat-adapter/teams/apiFetch-based Bot Connector calls for messages, updates, deletes, typing, and conversations
@chat-adapter/teams/graphFetch-based Microsoft Graph reads for chats, channels, channel messages, and replies
@chat-adapter/teams/formatTeams HTML, mention, Markdown-ish, and emoji string helpers
@chat-adapter/teams/cardsRuntime-free conversion from simple card objects and input requests to Adaptive Cards
@chat-adapter/teams/modalsRuntime-free Task Module Adaptive Card helpers and submit parsing

The webhook subpath parses Activities only. It does not verify Microsoft Bot Framework JWTs. Authenticate the request with the Microsoft Teams SDK request pipeline before handing the Activity to these helpers, or use createTeamsAdapter. The Connector helpers restrict serviceUrl to Microsoft-owned hosts, but that does not authenticate the Activity.

Webhooks

Teams sends Bot Framework Activity JSON. readTeamsWebhook reads the request body and classifies the Activity, but it intentionally does not perform JWT validation.

app/api/teams/route.ts
import { postTeamsMessage } from "@chat-adapter/teams/api";
import { readTeamsWebhook } from "@chat-adapter/teams/webhook";

export async function POST(request: Request) {
  // Authenticate the Bot Framework JWT before parsing the request. This
  // example assumes your routing layer already performed that verification.
  const payload = await readTeamsWebhook(request, {
    botAppId: process.env.TEAMS_APP_ID,
  });

  if (payload.kind === "message") {
    await postTeamsMessage({
      conversationId: payload.continuation.conversationId,
      credentials: {
        appId: process.env.TEAMS_APP_ID!,
        appPassword: process.env.TEAMS_APP_PASSWORD!,
        tenantId: payload.continuation.tenantId,
      },
      markdownText: `received: ${payload.text}`,
      serviceUrl: payload.continuation.serviceUrl,
    });
  }

  return new Response(null, { status: 200 });
}

parseTeamsWebhookBody returns typed payloads:

KindTeams surface
messageMessage activities
message_reactionReaction activities
card_actionAdaptive Card actions and Action.Submit message activities
dialog_openTask Module task/fetch invokes
dialog_submitTask Module task/submit invokes
conversation_updateConversation membership and install context updates
installation_updateApp installation updates
unsupportedValid Activities not normalized by this helper yet

Message-like payloads include continuation, which contains provider-native reply context:

type TeamsContinuation = {
  activityId?: string;
  channelId?: string;
  conversationId: string;
  replyToId?: string;
  serviceUrl: string;
  teamId?: string;
  tenantId?: string;
};

This is not a Chat SDK Thread. It is the durable Teams data you need to reply later with @chat-adapter/teams/api.

Bot Connector API

The API subpath calls the Bot Framework Connector REST API with fetch. It does not import @microsoft/teams.apps.

teams.ts
import {
  deleteTeamsMessage,
  postTeamsMessage,
  sendTeamsTyping,
  updateTeamsMessage,
} from "@chat-adapter/teams/api";

const credentials = {
  appId: process.env.TEAMS_APP_ID!,
  appPassword: process.env.TEAMS_APP_PASSWORD!,
  tenantId: process.env.TEAMS_APP_TENANT_ID!,
};

const posted = await postTeamsMessage({
  conversationId: "19:abc@thread.tacv2",
  credentials,
  markdownText: "**hello**",
  serviceUrl: "https://smba.trafficmanager.net/teams/",
});

await updateTeamsMessage({
  conversationId: "19:abc@thread.tacv2",
  credentials,
  messageId: posted.id,
  serviceUrl: "https://smba.trafficmanager.net/teams/",
  text: "updated",
});

await sendTeamsTyping({
  conversationId: "19:abc@thread.tacv2",
  credentials,
  serviceUrl: "https://smba.trafficmanager.net/teams/",
});

await deleteTeamsMessage({
  conversationId: "19:abc@thread.tacv2",
  credentials,
  messageId: posted.id,
  serviceUrl: "https://smba.trafficmanager.net/teams/",
});

Use accessToken in credentials when your runtime already owns Microsoft token acquisition. A direct accessToken must be scoped for the API you call. The Bot Connector subpath (/api) needs a https://api.botframework.com/.default token, while the Graph subpath (/graph) needs a https://graph.microsoft.com/.default token. Passing the same token to both will fail against one of them. When you supply appId/appPassword instead, each subpath requests the correct scope for you.

Graph

The Graph subpath reads Teams history with explicit Graph IDs. Unlike TeamsAdapter, it does not use the adapter state cache to infer teamId, channelId, or chatId.

import { listTeamsChannelMessages } from "@chat-adapter/teams/graph";

const messages = await listTeamsChannelMessages({
  channelId: "19:channel@thread.tacv2",
  credentials: {
    appId: process.env.TEAMS_APP_ID!,
    appPassword: process.env.TEAMS_APP_PASSWORD!,
    tenantId: process.env.TEAMS_APP_TENANT_ID!,
  },
  limit: 25,
  teamId: "19:team@thread.tacv2",
});

const latestText = messages.items[0]?.text;

Graph reads require the same Microsoft Graph permissions as the full adapter. Channel and group-chat reads can use RSC permissions; DM reads require Azure AD application permissions such as Chat.Read.All.

Formatting

Teams renders message text as HTML. The format subpath provides small helpers for custom runtimes:

import {
  formatTeamsMention,
  markdownToTeamsHtml,
  teamsHtmlToMarkdown,
} from "@chat-adapter/teams/format";

const html = markdownToTeamsHtml(
  `${formatTeamsMention("Ada")} approved **deploy v2.4.1**`
);
const markdown = teamsHtmlToMarkdown("<p>Hello <strong>world</strong></p>");

Use the full TeamsFormatConverter from @chat-adapter/teams when you need mdast conversion inside Chat SDK.

Cards

The cards subpath converts simple card objects into Adaptive Card JSON without importing the full chat JSX runtime.

cards.ts
import {
  cardToAdaptiveCard,
  cardToTeamsFallbackText,
} from "@chat-adapter/teams/cards";
import { postTeamsMessage } from "@chat-adapter/teams/api";

const card = {
  children: [
    { content: "deploy v2.4.1?", type: "text" },
    {
      children: [
        { id: "approve", label: "Approve", style: "primary", type: "button" },
        { id: "deny", label: "Deny", style: "danger", type: "button" },
      ],
      type: "actions",
    },
  ],
  title: "Deployment",
  type: "card",
} as const;

await postTeamsMessage({
  adaptiveCard: cardToAdaptiveCard(card),
  conversationId: payload.continuation.conversationId,
  credentials,
  serviceUrl: payload.continuation.serviceUrl,
  text: cardToTeamsFallbackText(card),
});

Use the full Chat SDK card JSX when you want cross-platform rendering. Use @chat-adapter/teams/cards when you are building a Teams-only runtime and want Adaptive Card output directly.

Table children render as the Adaptive Card Table element with grid lines on by default. The optional align, widths, verticalAlign, gridLines and gridStyle fields tune the grid, a table with empty headers renders without a header row, and a table with no columns emits no element.

A TextInput child renders as an Adaptive Card Input.Text with the same mapping the modal converter uses. Teams merges the card's input values into the Action.Submit payload, and the adapter reports them as event.values on the action. An input is required unless optional is set, and Teams blocks the card's buttons until a required input is filled. Teams enforces that in its own client, so validate event.values in the handler too.

A button with actionType="modal" is the exception. It renders with associatedInputs: "none", so it opens its dialog without waiting for the card's inputs, and its action arrives without event.values.

Teams flattens the inputs into the same payload that carries the button's own actionId, value, and msteams keys, so an input with one of those IDs would overwrite the button's data. Posting a card with a TextInput that uses one of those IDs throws a ValidationError. Modal inputs can use actionId and value, because a dialog's submit data doesn't carry a button's keys.

Modals

Teams Task Modules are invoke-based dialogs backed by Adaptive Cards. The modals subpath builds those cards and parses submit data.

import {
  modalToAdaptiveCard,
  parseTeamsDialogSubmitValues,
  toTeamsTaskModuleResponse,
} from "@chat-adapter/teams/modals";

const modal = {
  callbackId: "deploy",
  children: [
    { content: "Why deploy now?", type: "text" },
    { id: "reason", label: "Reason", type: "text_input" },
  ],
  title: "Deploy",
  type: "modal",
} as const;

const card = modalToAdaptiveCard(modal, { contextId: "deploy-1" });
const values = parseTeamsDialogSubmitValues(payload.value);

return Response.json(
  toTeamsTaskModuleResponse({ action: "update", modal }, { contextId: "deploy-1" })
);

Import boundaries

The low-level Teams subpaths are designed to avoid the full runtime import graph:

  • no chat import
  • no @chat-adapter/shared import
  • no @microsoft/teams.apps import
  • no full adapter import

The package still installs the full Teams adapter dependencies. The subpaths keep your source and bundle imports clean, but they are not a package-size split.

Conversation routing

Incoming thread IDs preserve the Teams conversation type when the legacy ID-prefix heuristic would route it incorrectly. When Teams omits conversationType, the adapter falls back to conversation.isGroup and the activity's team context. This keeps correctly classified IDs stable while selecting the buffered fallback for group chats whose IDs begin with a:. Thread IDs created by older adapter versions remain supported.

Troubleshooting

Run teams app doctor <appId> to diagnose common issues with bot registration, AAD app health, manifest consistency, and endpoint reachability.

Feature support

Messaging

FeatureSupported
Post message
Message replies
Edit message
Delete message
File uploads
StreamingNative (DMs) / Buffered
Scheduled messages

Rich content

FeatureSupported
Card formatAdaptive Cards
Buttons
Link buttons
Select menus
TablesAdaptive Card Table
Charts
Fields
Images in cards
Modals

Conversations

FeatureSupported
Slash commands
Mentions
Add reactions
Remove reactions
Typing indicator
Mark as read
Message edit events
Message delete events
DMs
Ephemeral messagesTargeted messages
User lookupCached
Parent subject
Native client
Custom API endpoint

Message history

FeatureSupported
Fetch messagesRequires Graph permissions
Fetch single message
Fetch thread info
Fetch channel messagesRequires Graph permissions
List threadsRequires Graph permissions
Fetch channel infoRequires Graph permissions
Post channel message

More adapters