{
  "title": "Use your Tailscale funnel URL as the messaging endpoint",
  "content": "json theme={null}\n   {\n     \"msteams\": {\n       \"enabled\": true,\n       \"appId\": \"<APP_ID>\",\n       \"appPassword\": \"<APP_PASSWORD>\",\n       \"tenantId\": \"<TENANT_ID>\",\n       \"webhook\": { \"port\": 3978, \"path\": \"/api/messages\" }\n     }\n   }\n   json theme={null}\n{\n  \"$schema\": \"https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json\",\n  \"manifestVersion\": \"1.23\",\n  \"version\": \"1.0.0\",\n  \"id\": \"00000000-0000-0000-0000-000000000000\",\n  \"name\": { \"short\": \"OpenClaw\" },\n  \"developer\": {\n    \"name\": \"Your Org\",\n    \"websiteUrl\": \"https://example.com\",\n    \"privacyUrl\": \"https://example.com/privacy\",\n    \"termsOfUseUrl\": \"https://example.com/terms\"\n  },\n  \"description\": { \"short\": \"OpenClaw in Teams\", \"full\": \"OpenClaw in Teams\" },\n  \"icons\": { \"outline\": \"outline.png\", \"color\": \"color.png\" },\n  \"accentColor\": \"#5B6DEF\",\n  \"bots\": [\n    {\n      \"botId\": \"11111111-1111-1111-1111-111111111111\",\n      \"scopes\": [\"personal\", \"team\", \"groupChat\"],\n      \"isNotificationOnly\": false,\n      \"supportsCalling\": false,\n      \"supportsVideo\": false,\n      \"supportsFiles\": true\n    }\n  ],\n  \"webApplicationInfo\": {\n    \"id\": \"11111111-1111-1111-1111-111111111111\"\n  },\n  \"authorization\": {\n    \"permissions\": {\n      \"resourceSpecific\": [\n        { \"name\": \"ChannelMessage.Read.Group\", \"type\": \"Application\" },\n        { \"name\": \"ChannelMessage.Send.Group\", \"type\": \"Application\" },\n        { \"name\": \"Member.Read.Group\", \"type\": \"Application\" },\n        { \"name\": \"Owner.Read.Group\", \"type\": \"Application\" },\n        { \"name\": \"ChannelSettings.Read.Group\", \"type\": \"Application\" },\n        { \"name\": \"TeamMember.Read.Group\", \"type\": \"Application\" },\n        { \"name\": \"TeamSettings.Read.Group\", \"type\": \"Application\" },\n        { \"name\": \"ChatMessage.Read.Chat\", \"type\": \"Application\" }\n      ]\n    }\n  }\n}\njson theme={null}\n{\n  \"msteams\": {\n    \"replyStyle\": \"thread\",\n    \"teams\": {\n      \"19:abc...@thread.tacv2\": {\n        \"channels\": {\n          \"19:xyz...@thread.tacv2\": {\n            \"replyStyle\": \"top-level\"\n          }\n        }\n      }\n    }\n  }\n}\nbash theme={null}\n   # Via Graph Explorer or curl with a valid token:\n   curl -H \"Authorization: Bearer $TOKEN\" \\\n     \"https://graph.microsoft.com/v1.0/sites/{hostname}:/{site-path}\"\n\n# Example: for a site at \"contoso.sharepoint.com/sites/BotFiles\"\n   curl -H \"Authorization: Bearer $TOKEN\" \\\n     \"https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/BotFiles\"\n\n# Response includes: \"id\": \"contoso.sharepoint.com,guid1,guid2\"\n   json5 theme={null}\n   {\n     channels: {\n       msteams: {\n         // ... other config ...\n         sharePointSiteId: \"contoso.sharepoint.com,guid1,guid2\",\n       },\n     },\n   }\n   json theme={null}\n{\n  \"action\": \"send\",\n  \"channel\": \"msteams\",\n  \"target\": \"user:<id>\",\n  \"card\": {\n    \"type\": \"AdaptiveCard\",\n    \"version\": \"1.5\",\n    \"body\": [{ \"type\": \"TextBlock\", \"text\": \"Hello!\" }]\n  }\n}\nbash theme={null}\nopenclaw message send --channel msteams \\\n  --target \"conversation:19:abc...@thread.tacv2\" \\\n  --card '{\"type\":\"AdaptiveCard\",\"version\":\"1.5\",\"body\":[{\"type\":\"TextBlock\",\"text\":\"Hello!\"}]}'\nbash theme={null}",
  "code_samples": [
    {
      "code": "## Teams Developer Portal (Alternative)\n\nInstead of manually creating a manifest ZIP, you can use the [Teams Developer Portal](https://dev.teams.microsoft.com/apps):\n\n1. Click **+ New app**\n2. Fill in basic info (name, description, developer info)\n3. Go to **App features** → **Bot**\n4. Select **Enter a bot ID manually** and paste your Azure Bot App ID\n5. Check scopes: **Personal**, **Team**, **Group Chat**\n6. Click **Distribute** → **Download app package**\n7. In Teams: **Apps** → **Manage your apps** → **Upload a custom app** → select the ZIP\n\nThis is often easier than hand-editing JSON manifests.\n\n## Testing the Bot\n\n**Option A: Azure Web Chat (verify webhook first)**\n\n1. In Azure Portal → your Azure Bot resource → **Test in Web Chat**\n2. Send a message - you should see a response\n3. This confirms your webhook endpoint works before Teams setup\n\n**Option B: Teams (after app installation)**\n\n1. Install the Teams app (sideload or org catalog)\n2. Find the bot in Teams and send a DM\n3. Check gateway logs for incoming activity\n\n## Setup (minimal text-only)\n\n1. **Install the Microsoft Teams plugin**\n   * From npm: `openclaw plugins install @openclaw/msteams`\n   * From a local checkout: `openclaw plugins install ./extensions/msteams`\n\n2. **Bot registration**\n   * Create an Azure Bot (see above) and note:\n     * App ID\n     * Client secret (App password)\n     * Tenant ID (single-tenant)\n\n3. **Teams app manifest**\n   * Include a `bot` entry with `botId = <App ID>`.\n   * Scopes: `personal`, `team`, `groupChat`.\n   * `supportsFiles: true` (required for personal scope file handling).\n   * Add RSC permissions (below).\n   * Create icons: `outline.png` (32x32) and `color.png` (192x192).\n   * Zip all three files together: `manifest.json`, `outline.png`, `color.png`.\n\n4. **Configure OpenClaw**",
      "language": "unknown"
    },
    {
      "code": "You can also use environment variables instead of config keys:\n\n   * `MSTEAMS_APP_ID`\n   * `MSTEAMS_APP_PASSWORD`\n   * `MSTEAMS_TENANT_ID`\n\n5. **Bot endpoint**\n   * Set the Azure Bot Messaging Endpoint to:\n     * `https://<host>:3978/api/messages` (or your chosen path/port).\n\n6. **Run the gateway**\n   * The Teams channel starts automatically when the plugin is installed and `msteams` config exists with credentials.\n\n## History context\n\n* `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt.\n* Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).\n* DM history can be limited with `channels.msteams.dmHistoryLimit` (user turns). Per-user overrides: `channels.msteams.dms[\"<user_id>\"].historyLimit`.\n\n## Current Teams RSC Permissions (Manifest)\n\nThese are the **existing resourceSpecific permissions** in our Teams app manifest. They only apply inside the team/chat where the app is installed.\n\n**For channels (team scope):**\n\n* `ChannelMessage.Read.Group` (Application) - receive all channel messages without @mention\n* `ChannelMessage.Send.Group` (Application)\n* `Member.Read.Group` (Application)\n* `Owner.Read.Group` (Application)\n* `ChannelSettings.Read.Group` (Application)\n* `TeamMember.Read.Group` (Application)\n* `TeamSettings.Read.Group` (Application)\n\n**For group chats:**\n\n* `ChatMessage.Read.Chat` (Application) - receive all group chat messages without @mention\n\n## Example Teams Manifest (redacted)\n\nMinimal, valid example with the required fields. Replace IDs and URLs.",
      "language": "unknown"
    },
    {
      "code": "### Manifest caveats (must-have fields)\n\n* `bots[].botId` **must** match the Azure Bot App ID.\n* `webApplicationInfo.id` **must** match the Azure Bot App ID.\n* `bots[].scopes` must include the surfaces you plan to use (`personal`, `team`, `groupChat`).\n* `bots[].supportsFiles: true` is required for file handling in personal scope.\n* `authorization.permissions.resourceSpecific` must include channel read/send if you want channel traffic.\n\n### Updating an existing app\n\nTo update an already-installed Teams app (e.g., to add RSC permissions):\n\n1. Update your `manifest.json` with the new settings\n2. **Increment the `version` field** (e.g., `1.0.0` → `1.1.0`)\n3. **Re-zip** the manifest with icons (`manifest.json`, `outline.png`, `color.png`)\n4. Upload the new zip:\n   * **Option A (Teams Admin Center):** Teams Admin Center → Teams apps → Manage apps → find your app → Upload new version\n   * **Option B (Sideload):** In Teams → Apps → Manage your apps → Upload a custom app\n5. **For team channels:** Reinstall the app in each team for new permissions to take effect\n6. **Fully quit and relaunch Teams** (not just close the window) to clear cached app metadata\n\n## Capabilities: RSC only vs Graph\n\n### With **Teams RSC only** (app installed, no Graph API permissions)\n\nWorks:\n\n* Read channel message **text** content.\n* Send channel message **text** content.\n* Receive **personal (DM)** file attachments.\n\nDoes NOT work:\n\n* Channel/group **image or file contents** (payload only includes HTML stub).\n* Downloading attachments stored in SharePoint/OneDrive.\n* Reading message history (beyond the live webhook event).\n\n### With **Teams RSC + Microsoft Graph Application permissions**\n\nAdds:\n\n* Downloading hosted contents (images pasted into messages).\n* Downloading file attachments stored in SharePoint/OneDrive.\n* Reading channel/chat message history via Graph.\n\n### RSC vs Graph API\n\n| Capability              | RSC Permissions      | Graph API                           |\n| ----------------------- | -------------------- | ----------------------------------- |\n| **Real-time messages**  | Yes (via webhook)    | No (polling only)                   |\n| **Historical messages** | No                   | Yes (can query history)             |\n| **Setup complexity**    | App manifest only    | Requires admin consent + token flow |\n| **Works offline**       | No (must be running) | Yes (query anytime)                 |\n\n**Bottom line:** RSC is for real-time listening; Graph API is for historical access. For catching up on missed messages while offline, you need Graph API with `ChannelMessage.Read.All` (requires admin consent).\n\n## Graph-enabled media + history (required for channels)\n\nIf you need images/files in **channels** or want to fetch **message history**, you must enable Microsoft Graph permissions and grant admin consent.\n\n1. In Entra ID (Azure AD) **App Registration**, add Microsoft Graph **Application permissions**:\n   * `ChannelMessage.Read.All` (channel attachments + history)\n   * `Chat.Read.All` or `ChatMessage.Read.All` (group chats)\n2. **Grant admin consent** for the tenant.\n3. Bump the Teams app **manifest version**, re-upload, and **reinstall the app in Teams**.\n4. **Fully quit and relaunch Teams** to clear cached app metadata.\n\n## Known Limitations\n\n### Webhook timeouts\n\nTeams delivers messages via HTTP webhook. If processing takes too long (e.g., slow LLM responses), you may see:\n\n* Gateway timeouts\n* Teams retrying the message (causing duplicates)\n* Dropped replies\n\nOpenClaw handles this by returning quickly and sending replies proactively, but very slow responses may still cause issues.\n\n### Formatting\n\nTeams markdown is more limited than Slack or Discord:\n\n* Basic formatting works: **bold**, *italic*, `code`, links\n* Complex markdown (tables, nested lists) may not render correctly\n* Adaptive Cards are supported for polls and arbitrary card sends (see below)\n\n## Configuration\n\nKey settings (see `/gateway/configuration` for shared channel patterns):\n\n* `channels.msteams.enabled`: enable/disable the channel.\n* `channels.msteams.appId`, `channels.msteams.appPassword`, `channels.msteams.tenantId`: bot credentials.\n* `channels.msteams.webhook.port` (default `3978`)\n* `channels.msteams.webhook.path` (default `/api/messages`)\n* `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)\n* `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs, UPNs, or display names). The wizard resolves names to IDs during setup when Graph access is available.\n* `channels.msteams.textChunkLimit`: outbound text chunk size.\n* `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.\n* `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).\n* `channels.msteams.mediaAuthAllowHosts`: allowlist for attaching Authorization headers on media retries (defaults to Graph + Bot Framework hosts).\n* `channels.msteams.requireMention`: require @mention in channels/groups (default true).\n* `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).\n* `channels.msteams.teams.<teamId>.replyStyle`: per-team override.\n* `channels.msteams.teams.<teamId>.requireMention`: per-team override.\n* `channels.msteams.teams.<teamId>.tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing.\n* `channels.msteams.teams.<teamId>.toolsBySender`: default per-team per-sender tool policy overrides (`\"*\"` wildcard supported).\n* `channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle`: per-channel override.\n* `channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention`: per-channel override.\n* `channels.msteams.teams.<teamId>.channels.<conversationId>.tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).\n* `channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender`: per-channel per-sender tool policy overrides (`\"*\"` wildcard supported).\n* `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)).\n\n## Routing & Sessions\n\n* Session keys follow the standard agent format (see [/concepts/session](/concepts/session)):\n  * Direct messages share the main session (`agent:<agentId>:<mainKey>`).\n  * Channel/group messages use conversation id:\n    * `agent:<agentId>:msteams:channel:<conversationId>`\n    * `agent:<agentId>:msteams:group:<conversationId>`\n\n## Reply Style: Threads vs Posts\n\nTeams recently introduced two channel UI styles over the same underlying data model:\n\n| Style                    | Description                                               | Recommended `replyStyle` |\n| ------------------------ | --------------------------------------------------------- | ------------------------ |\n| **Posts** (classic)      | Messages appear as cards with threaded replies underneath | `thread` (default)       |\n| **Threads** (Slack-like) | Messages flow linearly, more like Slack                   | `top-level`              |\n\n**The problem:** The Teams API does not expose which UI style a channel uses. If you use the wrong `replyStyle`:\n\n* `thread` in a Threads-style channel → replies appear nested awkwardly\n* `top-level` in a Posts-style channel → replies appear as separate top-level posts instead of in-thread\n\n**Solution:** Configure `replyStyle` per-channel based on how the channel is set up:",
      "language": "unknown"
    },
    {
      "code": "## Attachments & Images\n\n**Current limitations:**\n\n* **DMs:** Images and file attachments work via Teams bot file APIs.\n* **Channels/groups:** Attachments live in M365 storage (SharePoint/OneDrive). The webhook payload only includes an HTML stub, not the actual file bytes. **Graph API permissions are required** to download channel attachments.\n\nWithout Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot).\nBy default, OpenClaw only downloads media from Microsoft/Teams hostnames. Override with `channels.msteams.mediaAllowHosts` (use `[\"*\"]` to allow any host).\nAuthorization headers are only attached for hosts in `channels.msteams.mediaAuthAllowHosts` (defaults to Graph + Bot Framework hosts). Keep this list strict (avoid multi-tenant suffixes).\n\n## Sending files in group chats\n\nBots can send files in DMs using the FileConsentCard flow (built-in). However, **sending files in group chats/channels** requires additional setup:\n\n| Context                  | How files are sent                           | Setup needed                                    |\n| ------------------------ | -------------------------------------------- | ----------------------------------------------- |\n| **DMs**                  | FileConsentCard → user accepts → bot uploads | Works out of the box                            |\n| **Group chats/channels** | Upload to SharePoint → share link            | Requires `sharePointSiteId` + Graph permissions |\n| **Images (any context)** | Base64-encoded inline                        | Works out of the box                            |\n\n### Why group chats need SharePoint\n\nBots don't have a personal OneDrive drive (the `/me/drive` Graph API endpoint doesn't work for application identities). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link.\n\n### Setup\n\n1. **Add Graph API permissions** in Entra ID (Azure AD) → App Registration:\n   * `Sites.ReadWrite.All` (Application) - upload files to SharePoint\n   * `Chat.Read.All` (Application) - optional, enables per-user sharing links\n\n2. **Grant admin consent** for the tenant.\n\n3. **Get your SharePoint site ID:**",
      "language": "unknown"
    },
    {
      "code": "4. **Configure OpenClaw:**",
      "language": "unknown"
    },
    {
      "code": "### Sharing behavior\n\n| Permission                              | Sharing behavior                                          |\n| --------------------------------------- | --------------------------------------------------------- |\n| `Sites.ReadWrite.All` only              | Organization-wide sharing link (anyone in org can access) |\n| `Sites.ReadWrite.All` + `Chat.Read.All` | Per-user sharing link (only chat members can access)      |\n\nPer-user sharing is more secure as only the chat participants can access the file. If `Chat.Read.All` permission is missing, the bot falls back to organization-wide sharing.\n\n### Fallback behavior\n\n| Scenario                                          | Result                                             |\n| ------------------------------------------------- | -------------------------------------------------- |\n| Group chat + file + `sharePointSiteId` configured | Upload to SharePoint, send sharing link            |\n| Group chat + file + no `sharePointSiteId`         | Attempt OneDrive upload (may fail), send text only |\n| Personal chat + file                              | FileConsentCard flow (works without SharePoint)    |\n| Any context + image                               | Base64-encoded inline (works without SharePoint)   |\n\n### Files stored location\n\nUploaded files are stored in a `/OpenClawShared/` folder in the configured SharePoint site's default document library.\n\n## Polls (Adaptive Cards)\n\nOpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API).\n\n* CLI: `openclaw message poll --channel msteams --target conversation:<id> ...`\n* Votes are recorded by the gateway in `~/.openclaw/msteams-polls.json`.\n* The gateway must stay online to record votes.\n* Polls do not auto-post result summaries yet (inspect the store file if needed).\n\n## Adaptive Cards (arbitrary)\n\nSend any Adaptive Card JSON to Teams users or conversations using the `message` tool or CLI.\n\nThe `card` parameter accepts an Adaptive Card JSON object. When `card` is provided, the message text is optional.\n\n**Agent tool:**",
      "language": "unknown"
    },
    {
      "code": "**CLI:**",
      "language": "unknown"
    },
    {
      "code": "See [Adaptive Cards documentation](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below.\n\n## Target formats\n\nMSTeams targets use prefixes to distinguish between users and conversations:\n\n| Target type         | Format                           | Example                                             |\n| ------------------- | -------------------------------- | --------------------------------------------------- |\n| User (by ID)        | `user:<aad-object-id>`           | `user:40a1a0ed-4ff2-4164-a219-55518990c197`         |\n| User (by name)      | `user:<display-name>`            | `user:John Smith` (requires Graph API)              |\n| Group/channel       | `conversation:<conversation-id>` | `conversation:19:abc123...@thread.tacv2`            |\n| Group/channel (raw) | `<conversation-id>`              | `19:abc123...@thread.tacv2` (if contains `@thread`) |\n\n**CLI examples:**",
      "language": "unknown"
    }
  ],
  "headings": [
    {
      "level": "h2",
      "text": "Teams Developer Portal (Alternative)",
      "id": "teams-developer-portal-(alternative)"
    },
    {
      "level": "h2",
      "text": "Testing the Bot",
      "id": "testing-the-bot"
    },
    {
      "level": "h2",
      "text": "Setup (minimal text-only)",
      "id": "setup-(minimal-text-only)"
    },
    {
      "level": "h2",
      "text": "History context",
      "id": "history-context"
    },
    {
      "level": "h2",
      "text": "Current Teams RSC Permissions (Manifest)",
      "id": "current-teams-rsc-permissions-(manifest)"
    },
    {
      "level": "h2",
      "text": "Example Teams Manifest (redacted)",
      "id": "example-teams-manifest-(redacted)"
    },
    {
      "level": "h3",
      "text": "Manifest caveats (must-have fields)",
      "id": "manifest-caveats-(must-have-fields)"
    },
    {
      "level": "h3",
      "text": "Updating an existing app",
      "id": "updating-an-existing-app"
    },
    {
      "level": "h2",
      "text": "Capabilities: RSC only vs Graph",
      "id": "capabilities:-rsc-only-vs-graph"
    },
    {
      "level": "h3",
      "text": "With **Teams RSC only** (app installed, no Graph API permissions)",
      "id": "with-**teams-rsc-only**-(app-installed,-no-graph-api-permissions)"
    },
    {
      "level": "h3",
      "text": "With **Teams RSC + Microsoft Graph Application permissions**",
      "id": "with-**teams-rsc-+-microsoft-graph-application-permissions**"
    },
    {
      "level": "h3",
      "text": "RSC vs Graph API",
      "id": "rsc-vs-graph-api"
    },
    {
      "level": "h2",
      "text": "Graph-enabled media + history (required for channels)",
      "id": "graph-enabled-media-+-history-(required-for-channels)"
    },
    {
      "level": "h2",
      "text": "Known Limitations",
      "id": "known-limitations"
    },
    {
      "level": "h3",
      "text": "Webhook timeouts",
      "id": "webhook-timeouts"
    },
    {
      "level": "h3",
      "text": "Formatting",
      "id": "formatting"
    },
    {
      "level": "h2",
      "text": "Configuration",
      "id": "configuration"
    },
    {
      "level": "h2",
      "text": "Routing & Sessions",
      "id": "routing-&-sessions"
    },
    {
      "level": "h2",
      "text": "Reply Style: Threads vs Posts",
      "id": "reply-style:-threads-vs-posts"
    },
    {
      "level": "h2",
      "text": "Attachments & Images",
      "id": "attachments-&-images"
    },
    {
      "level": "h2",
      "text": "Sending files in group chats",
      "id": "sending-files-in-group-chats"
    },
    {
      "level": "h3",
      "text": "Why group chats need SharePoint",
      "id": "why-group-chats-need-sharepoint"
    },
    {
      "level": "h3",
      "text": "Setup",
      "id": "setup"
    },
    {
      "level": "h3",
      "text": "Sharing behavior",
      "id": "sharing-behavior"
    },
    {
      "level": "h3",
      "text": "Fallback behavior",
      "id": "fallback-behavior"
    },
    {
      "level": "h3",
      "text": "Files stored location",
      "id": "files-stored-location"
    },
    {
      "level": "h2",
      "text": "Polls (Adaptive Cards)",
      "id": "polls-(adaptive-cards)"
    },
    {
      "level": "h2",
      "text": "Adaptive Cards (arbitrary)",
      "id": "adaptive-cards-(arbitrary)"
    },
    {
      "level": "h2",
      "text": "Target formats",
      "id": "target-formats"
    }
  ],
  "url": "llms-txt#use-your-tailscale-funnel-url-as-the-messaging-endpoint",
  "links": []
}