Send a Microsoft Teams Channel Message with az account get-access-token and ChannelMessage.Send
Send a Microsoft Teams channel message via Microsoft Graph using a token from az account get-access-token and the ChannelMessage.Send permission: end-to-end how-to.
- #microsoft-teams
- #microsoft-graph
- #azure
- #troubleshooting
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
What You Are Building
You want to post a message to a Microsoft Teams channel from a script or pipeline, using an access token minted by az account get-access-token and the Microsoft Graph ChannelMessage.Send permission. The final call is a single POST to the Graph v1.0 endpoint:
POST https://graph.microsoft.com/v1.0/teams/{team-id}/channels/{channel-id}/messages
The tricky part is not the HTTP call — it is getting a token with the right audience and the right permission type. Get those two things correct and the message goes through on the first try.
The One Thing to Know First: Delegated, Not Application
ChannelMessage.Send is a delegated permission. For the vast majority of tenants, Microsoft Graph does not support sending channel messages with application permissions (app-only tokens). If you try to use a client-credentials / app-only token, you will get 403 Forbidden with Authorization_RequestDenied.
Application-permission channel posting only exists through Resource-Specific Consent (RSC) for a Teams app installed in that specific team, which is a different flow entirely. For a normal script, use a delegated token that carries a signed-in user’s identity — which is exactly what az account get-access-token gives you when you are logged in as a user with az login.
So the message is posted as you (the signed-in Azure CLI user), and that user must be a member of the target team.
Step-by-Step Resolution
- Sign in as a user (not a service principal) so the token is delegated:
az login
- Request a token whose audience is Microsoft Graph. The default
azaudience is ARM, not Graph, so you must specify the resource explicitly:
az account get-access-token \
--resource https://graph.microsoft.com \
--query accessToken -o tsv
The modern equivalent uses --scope instead of --resource:
az account get-access-token \
--scope https://graph.microsoft.com/.default \
--query accessToken -o tsv
Both produce a token with aud: https://graph.microsoft.com. If you omit this, the token is scoped to https://management.azure.com and Graph rejects it with InvalidAuthenticationToken.
- Capture the token in a variable:
TOKEN=$(az account get-access-token \
--resource https://graph.microsoft.com \
--query accessToken -o tsv)
- Find the team ID and channel ID. List teams you belong to, then list channels:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/me/joinedTeams" | jq '.value[] | {displayName, id}'
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/teams/{team-id}/channels" | jq '.value[] | {displayName, id}'
The channel ID looks like 19:abcdef...@thread.tacv2. URL-encode the @ and : if your client does not do it automatically, though most curl/requests clients handle it.
- Post the message:
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://graph.microsoft.com/v1.0/teams/{team-id}/channels/{channel-id}/messages" \
-d '{
"body": {
"content": "Deploy finished: build 1423 is live in production."
}
}'
A successful call returns 201 Created with the created chatMessage resource, including its id and createdDateTime.
- Send richer content using HTML when you need formatting:
{
"body": {
"contentType": "html",
"content": "<b>Deploy finished:</b> build 1423 is live in <i>production</i>."
}
}
The Full Request
For reference, the complete HTTP request looks like this:
POST https://graph.microsoft.com/v1.0/teams/1a2b3c/channels/19:xxxx@thread.tacv2/messages
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
Content-Type: application/json
{
"body": {
"contentType": "html",
"content": "Nightly backup completed successfully."
}
}
Verifying the Token Is Correct
Before you blame the endpoint, confirm the token actually targets Graph and carries the scope. Decode the middle segment of the JWT:
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '{aud, scp, upn}'
You want to see:
{
"aud": "https://graph.microsoft.com",
"scp": "ChannelMessage.Send ...",
"upn": "you@contoso.com"
}
If aud is https://management.core.windows.net/ or an ARM URL, you forgot --resource/--scope. If scp does not include ChannelMessage.Send, the permission has not been consented for the Azure CLI client on your tenant, and an admin needs to grant it.
Prevention and Best Practices
- Always pass
--resource https://graph.microsoft.com(or--scope .../.default) — never rely on the default audience. - Treat this as a delegated action: the CLI user must be a member of the target team, or you will see
403. - Do not attempt app-only automation for channel posts unless you have set up an RSC-enabled Teams app; use delegated tokens or a user-assigned managed identity that maps to a service account with team membership.
- Cache the team and channel IDs; they are stable, so you do not need to re-query them every run.
- For building and iterating on message payloads and automation snippets, the DevOps AI prompt library has ready-made Teams and Graph prompts.
Frequently Asked Questions
Why do I get 403 Forbidden when the token looks valid? ChannelMessage.Send is delegated only for most tenants. If your token is app-only (client credentials), Graph returns Authorization_RequestDenied. Use a delegated token from az login as a user who is a member of the team.
Do I need Azure AD admin consent? Yes, if ChannelMessage.Send has not been consented for the Azure CLI application on your tenant. An administrator grants it once, then scp will include the permission.
Which token audience does Graph require? The token aud claim must be https://graph.microsoft.com. Pass --resource https://graph.microsoft.com or --scope https://graph.microsoft.com/.default to az account get-access-token.
Can I post to a channel as an application with no user? Only through Resource-Specific Consent with an installed Teams app. Standard app-only tokens cannot call this endpoint, so most automation uses a delegated user identity.
How do I get the channel ID? Call GET /teams/{team-id}/channels with your Graph token and read the id field, which looks like 19:...@thread.tacv2. For more, see the Microsoft Teams guides.
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.