The Microsoft Teams connector is a Palantir-provided driver for Microsoft Teams.
To create a new Microsoft Teams source, follow the standard setup flow for Palantir-provided drivers, then use the sections below for Microsoft Teams-specific configuration and networking. For the complete property reference, see the official Microsoft Teams driver documentation ↗.
| Capability | Status |
|---|---|
| Exploration | 🟢 Generally available |
| Batch syncs | 🟢 Generally available |
| Incremental | 🟢 Generally available |
| OAuth 2.0 authentication | 🟢 Generally available |
| Table exports | 🟢 Generally available |
The Microsoft Teams connector reads your organization's Teams over the Microsoft Graph API. It surfaces teams, channels, chats, members, and messages as relational tables that you browse in the source explorer.
For scheduled reads, use a batch sync to write Teams data to a dataset. For interactive reads or writes, call Microsoft Graph in code from a function.
Authentication and Microsoft Graph permissions determine which Teams data the connector can access. See Authentication to choose an authentication method and Permissions by capability for the permissions required by each operation.
The Microsoft Teams connector authenticates to Microsoft Graph through Microsoft Entra ID. Use AuthScheme to select an authentication method:
AuthScheme | Authentication method | Use when |
|---|---|---|
AzureAD | Delegated user authentication | Source access should follow the permissions of a signed-in user, and interactive authorization is acceptable. |
AzureServicePrincipal | Service principal with a client secret | Source access should not be scoped to a signed-in user. Microsoft Graph permissions can provide tenant-wide access or access restricted to selected teams through resource-specific consent. |
AzureServicePrincipalCert | Service principal with a certificate | The use case is the same as AzureServicePrincipal, but your organization requires certificate-based credentials. |
AzureAD)With AuthScheme=AzureAD the source signs in as an interactive user and reads Graph as that user.
Register an application in the Microsoft Entra admin center ↗, then record its client ID, generate a client secret, and note your tenant (ID or a domain such as contoso.onmicrosoft.com).
CallbackURL property. Copy the URL that Foundry fills in and add it to the application as a Web redirect URI.Set AuthScheme=AzureAD, AzureTenant, OAuthClientId, OAuthClientSecret, and InitiateOAuth=REFRESH, then add the auto-filled CallbackURL property. Select Authorize and sign in as the user whose Teams data you want to read. Foundry completes the interactive OAuth exchange and refreshes the token automatically.

AzureServicePrincipal)With AuthScheme=AzureServicePrincipal, the source authenticates as an application rather than as a signed-in user. The application can read across the tenant, depending on its granted permissions.
Register an application in the Microsoft Entra admin center ↗ and record its client ID and your tenant, as for the delegated scheme. The permissions differ:
Microsoft documents the full flow under get access without a user ↗.
Set AuthScheme=AzureServicePrincipal, AzureTenant, OAuthClientId, and OAuthClientSecret. There is no Authorize step. For certificate credentials, set AuthScheme=AzureServicePrincipalCert instead and configure the certificate properties from the official Microsoft Teams driver documentation ↗.
Grant only the Microsoft Graph permissions required for your use case. Your tenant's consent policies may impose additional restrictions. For the complete list, see the Microsoft Graph permissions reference ↗.
| What you want to do | Delegated | Application |
|---|---|---|
| Discover teams and channels | Team.ReadBasic.All and Channel.ReadBasic.All. Requires user consent. | Team.ReadBasic.All and Channel.ReadBasic.All. Requires administrator consent. |
| Read channel message content | ChannelMessage.Read.All. Requires administrator consent. | ChannelMessage.Read.All provides tenant-wide access and requires administrator consent. ChannelMessage.Read.Group provides access to an installed team and requires resource-specific consent for that team. |
| Read chats and chat messages | — | Chat.Read.All. Requires administrator consent. |
| List members and owners | GroupMember.Read.All, TeamMember.Read.All, and ChannelMember.Read.All. Requires administrator consent. | GroupMember.Read.All, TeamMember.Read.All, and ChannelMember.Read.All. Requires administrator consent. |
| Send a channel message | ChannelMessage.Send. Requires user consent. | ChannelMessage.Send.Group. Requires resource-specific consent for each team where the application is installed. |
To read Microsoft Teams data into Foundry, select a table in the source explorer and set up a batch sync. With delegated authentication, the sync reads data available to the signed-in user. With service principal authentication, access depends on the permissions granted to the application and can cover the tenant or selected teams. See Authentication to configure access.
Microsoft throttles GET /teams/{team-id}/channels/{channel-id}/messages at 200 requests per second per application, 20 requests per second per application per tenant, and 1 request per second per channel. See the Microsoft Teams service limits ↗.
The properties below are mandatory or recommended.
| Property | Required? | Description | Default |
|---|---|---|---|
AuthScheme ↗ | Recommended | Specifies the type of authentication to use when connecting to Microsoft Teams. If this property is left blank, the default authentication is used. | AzureServicePrincipal |
AzureTenant ↗ | Recommended | Identifies the Microsoft Teams tenant being used to access data, either by name (for example, contoso.onmicrosoft.com) or ID. (Conditional). | — |
InitiateOAuth ↗ | Recommended | Specifies the process for obtaining or refreshing the OAuth access token, which maintains user access while an authenticated, authorized user is working. | GETANDREFRESH |
OAuthClientId ↗ | Recommended | Specifies the client Id that was assigned when the custom OAuth application was created. (Also known as the consumer key.) This ID registers the custom application with the OAuth authorization server. | — |
OAuthClientSecret ↗ | Recommended | Specifies the client secret that was assigned when the custom OAuth application was created. (Also known as the consumer secret). This secret registers the custom application with the OAuth authorization server. | — |
OAuthGrantType ↗ | Recommended | Specifies the grant type for the chosen OAuth flow. This value should be the same as the grant_type that was set during OAuth custom application creation. | CLIENT |
The table below lists the domains that the source must be able to access to run.
For each domain, add a corresponding egress policy. If the source is hosted on-premises and not directly reachable from Foundry, use an agent proxy egress policy instead. The agent host itself must also be able to reach the listed domains. See using an agent as a proxy for details.
| Domain | Required |
|---|---|
| graph.microsoft.com | If AzureEnvironment=GLOBAL (default) |
| login.microsoftonline.com | If AuthScheme=AzureAD (default), AzureServicePrincipal, AzureServicePrincipalCert AND AzureEnvironment=GLOBAL (default) |
| microsoftgraph.chinacloudapi.cn | If AzureEnvironment=CHINA |
| login.chinacloudapi.cn | If AuthScheme=AzureAD (default), AzureServicePrincipal, AzureServicePrincipalCert AND AzureEnvironment=CHINA |
| graph.microsoft.us | If AzureEnvironment=USGOVT |
| login.microsoftonline.us | If AuthScheme=AzureAD (default), AzureServicePrincipal, AzureServicePrincipalCert AND AzureEnvironment=USGOVT or USGOVTDOD |
| dod-graph.microsoft.us | If AzureEnvironment=USGOVTDOD |
This connector supports OAuth 2.0 authentication. Follow the OAuth 2.0 guidance for Palantir-provided drivers to configure and authorize the connection.
This connector supports table exports. Learn how to set up a table export.
To read channel messages from a pipeline or function, call Microsoft Graph using a Microsoft Teams source. To send messages as a signed-in user, use a REST API source configured with an outbound application. The examples below demonstrate both workflows in Python and TypeScript.
To run these examples, import your Microsoft Teams source into your code repository. Configure the source with service principal authentication and the permissions required to read channel messages.
Replace the placeholders with your source, tenant, application, team, and channel identifiers. You can find the team and channel identifiers in the Teams and Channels tables in the source explorer. In the TypeScript v1 example, replace MicrosoftTeamsSource with the identifier generated from your source's API name.
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77"""Read Microsoft Teams channel-message metadata with app-only Microsoft Graph OAuth.""" import pandas as pd import requests from transforms.api import Output, transform from transforms.external.systems import ResolvedSource, Source, external_systems APP_ONLY_SOURCE_RID = "<source_rid>" TENANT_ID = "<tenant_id>" CLIENT_ID = "<client_id>" TEAM_ID = "<team_id>" CHANNEL_ID = "<channel_id>" TOKEN_URL = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" MESSAGES_URL = ( f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/channels/" f"{CHANNEL_ID}/messages" ) OUTPUT_PATH = "<output_dataset_rid>" def session_and_access_token(source: ResolvedSource) -> tuple[requests.Session, str]: session = requests.Session() proxy_uri = source.get_https_proxy_uri() if proxy_uri: session.proxies.update({"http": proxy_uri, "https": proxy_uri}) token_response = session.post( TOKEN_URL, data={ "client_id": CLIENT_ID, "client_secret": source.get_secret("OAuthClientSecret"), "scope": "https://graph.microsoft.com/.default", "grant_type": "client_credentials", }, timeout=30, ) token_response.raise_for_status() access_token = token_response.json().get("access_token") if not access_token: raise RuntimeError("Microsoft token response did not contain an access token") return session, access_token @external_systems(teams=Source(APP_ONLY_SOURCE_RID)) @transform.using(output=Output(OUTPUT_PATH)) def msteams_app_only_read(teams: ResolvedSource, output) -> None: """Write stable, non-content message metadata for the fixed Teams channel.""" session, access_token = session_and_access_token(teams) response = session.get( MESSAGES_URL, params={"$top": 20}, headers={"Authorization": f"Bearer {access_token}"}, timeout=30, ) response.raise_for_status() rows = [] for message in response.json().get("value", []): body = message.get("body") or {} rows.append( { "http_status": response.status_code, "id": message.get("id"), "reply_to_id": message.get("replyToId"), "etag": message.get("etag"), "created_date_time": message.get("createdDateTime"), "last_modified_date_time": message.get("lastModifiedDateTime"), "deleted_date_time": message.get("deletedDateTime"), "message_type": message.get("messageType"), "importance": message.get("importance"), "locale": message.get("locale"), "content_type": body.get("contentType"), "web_url": message.get("webUrl"), } ) output.write_table(pd.DataFrame(rows))
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84"""Read Microsoft Teams channel-message metadata with app-only Microsoft Graph OAuth.""" import json import requests from functions.api import UserFacingError, function from functions.sources import get_source SOURCE_ALIAS = "<source_alias>" TENANT_ID = "<tenant_id>" CLIENT_ID = "<client_id>" TEAM_ID = "<team_id>" CHANNEL_ID = "<channel_id>" TOKEN_URL = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" MESSAGES_URL = ( f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/channels/" f"{CHANNEL_ID}/messages" ) @function(sources=[SOURCE_ALIAS]) def list_channel_messages() -> str: """Return stable, non-content metadata for recent messages in the fixed channel.""" source = get_source(SOURCE_ALIAS) session = requests.Session() proxy_uri = source.get_https_proxy_uri() if proxy_uri: session.proxies.update({"http": proxy_uri, "https": proxy_uri}) token_response = session.post( TOKEN_URL, data={ "client_id": CLIENT_ID, "client_secret": source.get_secret("OAuthClientSecret"), "scope": "https://graph.microsoft.com/.default", "grant_type": "client_credentials", }, timeout=30, ) if not token_response.ok: raise UserFacingError( f"Microsoft token exchange failed with HTTP {token_response.status_code}." ) access_token = token_response.json().get("access_token") if not access_token: raise UserFacingError("Microsoft token response did not contain an access token.") response = session.get( MESSAGES_URL, params={"$top": 20}, headers={"Authorization": f"Bearer {access_token}"}, timeout=30, ) if not response.ok: raise UserFacingError( f"Microsoft Graph message read failed with HTTP {response.status_code}." ) messages = [] for message in response.json().get("value", []): body = message.get("body") or {} messages.append( { "id": message.get("id"), "replyToId": message.get("replyToId"), "etag": message.get("etag"), "createdDateTime": message.get("createdDateTime"), "lastModifiedDateTime": message.get("lastModifiedDateTime"), "deletedDateTime": message.get("deletedDateTime"), "messageType": message.get("messageType"), "importance": message.get("importance"), "locale": message.get("locale"), "contentType": body.get("contentType"), "webUrl": message.get("webUrl"), } ) return json.dumps( { "httpStatus": response.status_code, "messageCount": len(messages), "messages": messages, } )
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93import { ExternalSystems, Function, UserFacingError, } from "@foundry/functions-api"; import { MicrosoftTeamsSource, } from "@foundry/external-systems/sources"; const TENANT_ID = "<tenant_id>"; const CLIENT_ID = "<client_id>"; const TEAM_ID = "<team_id>"; const CHANNEL_ID = "<channel_id>"; const TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`; const MESSAGES_URL = `https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${encodeURIComponent(CHANNEL_ID)}/messages?$top=20`; interface TokenResponse { access_token?: string; } interface ChannelMessage { id?: string; replyToId?: string; etag?: string; createdDateTime?: string; lastModifiedDateTime?: string; deletedDateTime?: string; messageType?: string; importance?: string; locale?: string; body?: { contentType?: string }; webUrl?: string; } interface ChannelMessagesResponse { value?: ChannelMessage[]; } export class TeamsGraphFunctions { @ExternalSystems({ sources: [MicrosoftTeamsSource] }) @Function() public async listChannelMessages(): Promise<string> { const tokenResponse = await MicrosoftTeamsSource.fetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: MicrosoftTeamsSource.getSecret("OAuthClientSecret"), scope: "https://graph.microsoft.com/.default", grant_type: "client_credentials", }).toString(), }); if (!tokenResponse.ok) { throw new UserFacingError( `Microsoft token exchange failed with HTTP ${tokenResponse.status}.`, ); } const tokenPayload = await tokenResponse.json() as TokenResponse; if (!tokenPayload.access_token) { throw new UserFacingError("Microsoft token response did not contain an access token."); } const response = await MicrosoftTeamsSource.fetch(MESSAGES_URL, { method: "GET", headers: { Authorization: `Bearer ${tokenPayload.access_token}` }, }); if (!response.ok) { throw new UserFacingError( `Microsoft Graph message read failed with HTTP ${response.status}.`, ); } const payload = await response.json() as ChannelMessagesResponse; const messages = (payload.value ?? []).map((message) => ({ id: message.id, replyToId: message.replyToId, etag: message.etag, createdDateTime: message.createdDateTime, lastModifiedDateTime: message.lastModifiedDateTime, deletedDateTime: message.deletedDateTime, messageType: message.messageType, importance: message.importance, locale: message.locale, contentType: message.body?.contentType, webUrl: message.webUrl, })); return JSON.stringify({ httpStatus: response.status, messageCount: messages.length, messages, }); } }
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96import { UserFacingError } from "@osdk/functions"; import { getFetch, getSource } from "@palantir/functions-sources"; const APP_ONLY_SOURCE_RID = "<source_rid>"; const TENANT_ID = "<tenant_id>"; const CLIENT_ID = "<client_id>"; const TEAM_ID = "<team_id>"; const CHANNEL_ID = "<channel_id>"; const TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`; const MESSAGES_URL = `https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${encodeURIComponent(CHANNEL_ID)}/messages?$top=20`; export const config = { sources: [APP_ONLY_SOURCE_RID], }; interface TokenResponse { access_token?: string; } interface ChannelMessage { id?: string; replyToId?: string; etag?: string; createdDateTime?: string; lastModifiedDateTime?: string; deletedDateTime?: string; messageType?: string; importance?: string; locale?: string; body?: { contentType?: string }; webUrl?: string; } interface ChannelMessagesResponse { value?: ChannelMessage[]; } /** Returns stable, non-content metadata for recent messages in the fixed channel. */ export default async function listChannelMessages(): Promise<string> { const source = await getSource({ rid: APP_ONLY_SOURCE_RID }); const clientSecret = source.secrets.OAuthClientSecret; if (!clientSecret) { throw new UserFacingError("Microsoft Teams source is missing OAuthClientSecret."); } const sourceFetch = await getFetch(source); const tokenResponse = await sourceFetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: clientSecret, scope: "https://graph.microsoft.com/.default", grant_type: "client_credentials", }).toString(), }); if (!tokenResponse.ok) { throw new UserFacingError( `Microsoft token exchange failed with HTTP ${tokenResponse.status}.`, ); } const tokenPayload = (await tokenResponse.json()) as TokenResponse; if (!tokenPayload.access_token) { throw new UserFacingError("Microsoft token response did not contain an access token."); } const response = await sourceFetch(MESSAGES_URL, { method: "GET", headers: { Authorization: `Bearer ${tokenPayload.access_token}` }, }); if (!response.ok) { throw new UserFacingError( `Microsoft Graph message read failed with HTTP ${response.status}.`, ); } const payload = (await response.json()) as ChannelMessagesResponse; const messages = (payload.value ?? []).map((message) => ({ id: message.id, replyToId: message.replyToId, etag: message.etag, createdDateTime: message.createdDateTime, lastModifiedDateTime: message.lastModifiedDateTime, deletedDateTime: message.deletedDateTime, messageType: message.messageType, importance: message.importance, locale: message.locale, contentType: message.body?.contentType, webUrl: message.webUrl, })); return JSON.stringify({ httpStatus: response.status, messageCount: messages.length, messages, }); }
To send a channel message as a signed-in user, configure a REST API source for https://graph.microsoft.com with an outbound application. Grant the permissions required to send channel messages, then authorize the outbound application as the user who will send the messages.
The examples below send a message from a Python or TypeScript v2 function. Import the REST API source into your Functions repository and replace the source, team, and channel identifiers. Each example retrieves the user's access token and includes it in the request's Authorization header.
For application access to selected teams, use ChannelMessage.Send.Group with resource-specific consent and the application-token pattern from the read examples. Teamwork.Migrate.All is intended for historical message import ↗, not for sending new channel messages.
Before using this function directly in Workshop or in an automated workflow, authorize the outbound application through a function-backed action. The function uses the calling user's authorization. If that authorization expires or is revoked, the user must authorize again through an interactive workflow.
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80import json import requests from external_systems.sources import OauthCredentials from functions.api import function from functions.sources import get_source SOURCE_ALIAS = "<source_alias>" TEAM_ID = "<team_id>" CHANNEL_ID = "<channel_id>" MESSAGES_URL = ( f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/channels/" f"{CHANNEL_ID}/messages" ) @function(sources=[SOURCE_ALIAS]) def send_channel_message(text: str) -> str: """Post a message and return only sanitized status and identifiers.""" content = text.strip() if not content: return json.dumps( { "httpStatus": None, "errorCode": "InvalidInput", "errorMessage": "Message text must not be empty.", } ) source = get_source(SOURCE_ALIAS) credentials = source.get_session_credentials().get() if not isinstance(credentials, OauthCredentials) or not credentials.access_token: return json.dumps( { "httpStatus": None, "errorCode": "DelegatedOAuthUnavailable", "errorMessage": "Delegated Microsoft Graph OAuth is unavailable.", } ) session = requests.Session() proxy_uri = source.get_https_proxy_uri() if proxy_uri: session.proxies.update({"http": proxy_uri, "https": proxy_uri}) response = session.post( MESSAGES_URL, headers={ "Authorization": f"Bearer {credentials.access_token}", "Content-Type": "application/json", }, json={"body": {"content": content}}, timeout=30, ) if not response.ok: error_code = None error_message = f"Microsoft Graph returned HTTP {response.status_code}." try: error = response.json().get("error") or {} error_code = error.get("code") if error.get("message"): error_message = error["message"][:1000] except (AttributeError, ValueError): pass return json.dumps( { "httpStatus": response.status_code, "errorCode": error_code, "errorMessage": error_message, } ) message = response.json() return json.dumps( { "httpStatus": response.status_code, "messageId": message.get("id"), "webUrl": message.get("webUrl"), } )
Copied!1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77import { getFetch, getSource } from "@palantir/functions-sources"; const DELEGATED_SOURCE_RID = "<source_rid>"; const TEAM_ID = "<team_id>"; const CHANNEL_ID = "<channel_id>"; const MESSAGES_URL = `https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${encodeURIComponent(CHANNEL_ID)}/messages`; export const config = { sources: [DELEGATED_SOURCE_RID], }; interface PostedMessageResponse { id?: string; webUrl?: string; } interface GraphErrorResponse { error?: { code?: string; message?: string; }; } /** Posts one message and returns only sanitized status and identifiers. */ export default async function sendChannelMessage(text: string): Promise<string> { const content = text.trim(); if (!content) { return JSON.stringify({ httpStatus: null, errorCode: "InvalidInput", errorMessage: "Message text must not be empty.", }); } const source = await getSource({ rid: DELEGATED_SOURCE_RID }); const credentials = await source.sessionCredentials?.get(); if (!credentials || credentials.type !== "oauth" || !credentials.accessToken) { return JSON.stringify({ httpStatus: null, errorCode: "DelegatedOAuthUnavailable", errorMessage: "Delegated Microsoft Graph OAuth is unavailable.", }); } const sourceFetch = await getFetch(source); const response = await sourceFetch(MESSAGES_URL, { method: "POST", headers: { Authorization: `Bearer ${credentials.accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ body: { content } }), }); if (!response.ok) { let errorCode: string | undefined; let errorMessage = `Microsoft Graph returned HTTP ${response.status}.`; try { const payload = (await response.json()) as GraphErrorResponse; errorCode = payload.error?.code; errorMessage = payload.error?.message?.slice(0, 1000) ?? errorMessage; } catch { // Keep the bounded, status-only error when Graph does not return JSON. } return JSON.stringify({ httpStatus: response.status, errorCode, errorMessage, }); } const message = (await response.json()) as PostedMessageResponse; return JSON.stringify({ httpStatus: response.status, messageId: message.id, webUrl: message.webUrl, }); }