Teams APIでメッセージを送る方法は、Microsoft GraphのchatMessage送信API、Workflowsアプリの受信Webhook、Teams SDKで作るBotの3つです。どれを選ぶかで、投稿者の表示、使える権限、メンションやカードの書き方が変わります。この記事で扱うのは、Pythonのコードを使って、チャネル投稿とスレッド返信、ユーザーへのメンション、1対1チャットへの送信、画像とAdaptive Cardの添付までを経路ごとに順に組み立てる手順です。内容は2026年11月7日時点のMicrosoft Learnの記述に基づきます。
まとめ:Teams APIのメッセージ送信は投稿者の表示と無人実行の有無で経路を決める
利用者本人の名前で投稿するならGraph、システムから通知を流すだけならWorkflowsの受信Webhook、利用者の応答を受け取るならBotを選びます。Graphの送信APIはアプリケーション権限が移行用の Teamwork.Migrate.All に限られるため、夜間バッチのような無人の処理からは通常の投稿ができません。
メンションは、Graphでは本文の <at id="0"> と mentions 配列のidを対応させ、WorkflowsではAdaptive Cardの msteams.entities にUPNかEntraのオブジェクトIDを書きます。送信のPOSTは1チャネル・1チャットとも毎秒1件が上限なので、まとめて送る処理にはキューを挟んでください。
Teams APIでメッセージを送る3経路を送れる内容と前提条件で比べる
経路ごとの権限の違いや、Graphで扱えるTeamsの範囲は、親記事のTeams APIとは:Graphでできることと権限設計で整理しています。ここでは送信に絞り、何を送れるかを比べます。
メンション・スレッド返信・1対1チャット・カードの対応を経路別に比較
| 送りたいもの | Graph | Workflows | Bot |
|---|---|---|---|
| ユーザーへのメンション | 可 | 可(カード内) | 可 |
| スレッドへの返信 | 可 | フロー次第 | 可 |
| 1対1チャット | 可(委任) | フロー次第 | 可(会話IDが前提) |
| Adaptive Card | OpenUrlのみ | 可 | 可 |
| 投稿者の表示 | サインインした人 | フローの設定 | Botの名前 |
表の「フロー次第」は、Workflowsの側で投稿先や返信先を組む作り方になるという意味です。コードの側はURLにJSONを送るだけで、どこに出すかはフローの設定で決まります。
委任アクセスが前提のGraphを無人のバッチ通知に使えない理由
チャネル送信APIの権限表では、委任アクセスの最小権限が ChannelMessage.Send、アプリケーション権限は Teamwork.Migrate.All だけです。注記には、アプリケーション権限は他システムからの移行にのみ対応し、将来は取り込んだデータ量に応じた追加料金がかかる可能性もあると書かれています。
クライアント資格情報のトークンでは普段の通知を書き込めないため、無人の処理はWorkflowsかBotへ回します。サービスアカウントで委任サインインさせる回避策は、パスワードと多要素認証の管理という別の負担を生むので勧めません。
GraphのchatMessage送信APIでチャネル投稿とメンション付き返信を送る
Graphで送るときの本文は、chatMessageの作成APIに渡すJSONです。必須は body だけで、メンションや添付は任意のプロパティで足していきます。
チャネル投稿・スレッド返信・チャット送信の3つのエンドポイント
| 送信先 | POSTする先 | 委任の最小権限 |
|---|---|---|
| チャネルの新規投稿 | チャネルのmessages | ChannelMessage.Send |
| 投稿への返信 | 投稿のreplies | ChannelMessage.Send |
| 既存のチャット | チャットのmessages | ChatMessage.Send |
パスはそれぞれ /teams/{team-id}/channels/{channel-id}/messages、その末尾に /{message-id}/replies を足したもの、/chats/{chat-id}/messages です。どれも成功すると 201 Created と作成されたメッセージが返る仕様です。返信APIのリファレンスの例では、返信の応答に親の投稿IDが replyToId として入ります。返信先には、新規投稿で返ってきた id を保存しておいて使います。
本文のatタグとmentions配列のidを一致させるメンションの書き方
メンションは、本文のHTMLに <at id="0">表示名</at> を書き、同じ番号の要素を mentions 配列に入れて組みます。配列の要素には mentionText と、相手のEntraユーザーIDを持つ mentioned.user を指定します。
2人以上に送るときは、idを0、1、2と振って本文と配列の両方に並べます。idの対応がずれると、名前が太字で出ても通知が飛ばないメンションになります。表示名に記号が含まれる場合は、HTMLとしてエスケープしてから本文に入れてください。
PythonとMSALで投稿してからメンション付きでスレッド返信するコード
次のコードは pip install msal requests で動きます。アプリ登録で委任の ChannelMessage.Send を追加し、パブリッククライアントフローを許可しておく前提です。登録の手順は親記事で扱っています。
# teams_send.py : 委任アクセスでチャネルに投稿し、そのスレッドへメンション付きで返信する
import html, os, time
import msal, requests
GRAPH = "https://graph.microsoft.com/v1.0"
TEAM_ID = os.environ["TEAM_ID"]
CHANNEL_ID = os.environ["CHANNEL_ID"]
USER_ID = os.environ["MENTION_USER_ID"] # メンション相手の Entra オブジェクト ID
USER_NAME = os.environ["MENTION_USER_NAME"] # 本文に出す表示名
def get_token():
app = msal.PublicClientApplication(
os.environ["CLIENT_ID"],
authority=f"https://login.microsoftonline.com/{os.environ['TENANT_ID']}")
flow = app.initiate_device_flow(scopes=["ChannelMessage.Send"])
print(flow["message"]) # 表示された URL でサインインする
result = app.acquire_token_by_device_flow(flow)
if "access_token" not in result:
raise RuntimeError(result.get("error_description"))
return result["access_token"]
def post(token, path, payload):
for _ in range(5):
r = requests.post(GRAPH + path, json=payload, timeout=30,
headers={"Authorization": f"Bearer {token}"})
if r.status_code == 429: # 調整制限は Retry-After の秒数だけ待つ
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
r.raise_for_status()
return r.json()
raise RuntimeError("再送の上限に達しました")
token = get_token()
base = f"/teams/{TEAM_ID}/channels/{CHANNEL_ID}/messages"
root = post(token, base, {
"body": {"contentType": "html", "content": "<b>10月分の請求書</b>を発行しました。"}})
name = html.escape(USER_NAME)
reply = post(token, f"{base}/{root['id']}/replies", {
"body": {"contentType": "html",
"content": f'<at id="0">{name}</at> 差し戻しが2件あります。確認をお願いします。'},
"mentions": [{
"id": 0,
"mentionText": USER_NAME,
"mentioned": {"user": {"id": USER_ID, "displayName": USER_NAME,
"userIdentityType": "aadUser"}}}]})
print(root["id"], reply["id"], reply.get("webUrl"))
投稿と返信はどちらもサインインした利用者の名前で表示されます。チームIDとチャネルIDは、親記事のコードのように一覧APIで一度調べ、設定に固定しておくと名前の変更で壊れません。
1対1チャットへの送信と画像・カードの添付をGraphで組む手順
チャネルではなく特定の人に個別に送りたい場合や、画像を本文に埋め込みたい場合は、チャットとホストされたコンテンツのAPIを組み合わせます。
チャット作成APIが既存の1対1チャットを返す仕様を使った送信の流れ
チャット送信APIは既存のチャットにしか送れません。そこで先にチャット作成APIを呼びます。chatType に oneOnOne を指定し、送信者と相手の2人をメンバーに入れて POST /chats を送る形です。
リファレンスには、2人の間の1対1チャットは1つしか存在できず、既にあれば新しく作らずに既存のチャットを返すと書かれています。毎回作成APIを呼んで返ってきたIDに送れば、チャットIDを自前で管理する必要はありません。作成には委任の Chat.Create が要り、送信には別に ChatMessage.Send が要ります。
hostedContentsで画像を埋め込むときにtemporaryIdを本文と合わせる
グラフやスクリーンショットを本文に出すには、hostedContents 配列に画像を入れます。各要素の contentBytes にBase64でエンコードした画像、contentType に image/png などを指定する形式です。本文の <img src="../hostedContents/1/$value"> の「1」と、要素の @microsoft.graph.temporaryId を同じ値にします。
この対応は、chatMessage作成APIの例と注記で示されています。値がずれると画像のない投稿になるので、複数枚を送る処理では連番を1か所で振ってください。容量の大きい資料はSharePointに置き、ファイル添付として送る方が受け手も扱いやすくなります。
GraphのカードはOpenUrlだけ:ボタンの応答を受けるならBotに切り替える
Graphでも attachments にカードを入れ、本文の <attachment id="…"> と添付のidを一致させれば投稿できます。ただし、作成APIの説明ではGraphが対応するカードのアクションは OpenUrl だけで、ほかのアクションにはBotが必要とされています。
「承認」「却下」のボタンを押した結果を業務システムに戻す要件がある時点で、Graphの投稿では完結しません。リンクを開かせるだけのカードならGraph、押下を受け取るならBotかWorkflowsと、最初の設計で振り分けてください。
Workflowsの受信WebhookへAdaptive Cardとメンションを送る実装
Office 365コネクタと従来の受信Webhookは2026年5月に廃止され、現在はTeamsのWorkflowsアプリが受け口になります。Webhookの仕組みそのものはIncoming Webhookとはで解説しているので、ここでは送る側の実装に絞ります。
チャネルのWorkflowsテンプレートからWebhookのURLを発行する手順
受信Webhookの公式ページの手順は次のとおりです。
- 投稿先のチャネルの「…」から「Workflows」を開く
- 「Send webhook alerts to a channel」などのテンプレートを選ぶ
- 投稿先などのパラメーターを設定して保存する
- 作成されたワークフローのWebhookリンクをコピーする
発行されたURLは、知っていれば誰でも投稿できる鍵として扱います。ソースコードに直書きせず、環境変数やシークレットの保管庫から読み込んでください。
UPNでメンションするAdaptive CardをWebhookへ送るコード
カード書式のページによると、受信WebhookのAdaptive CardではUPNまたはEntraのオブジェクトIDでユーザーをメンションできます。本文の <at> と、msteams.entities の text を同じ文字列にするのが要点です。
# workflow_notify.py : Workflows の Webhook URL へメンション付き Adaptive Card を送る
import os, time
import requests
WEBHOOK_URL = os.environ["WEBHOOK_URL"]
UPN = os.environ["MENTION_UPN"] # メンション相手の UPN(Entra のオブジェクト ID でもよい)
NAME = os.environ["MENTION_USER_NAME"]
card = {
"type": "message",
"attachments": [{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{"type": "TextBlock", "weight": "Bolder", "text": "在庫引当エラー"},
{"type": "TextBlock", "wrap": True,
"text": f"<at>{NAME}</at> 受注3件の引当が失敗しました。"},
],
"msteams": {"entities": [{
"type": "mention",
"text": f"<at>{NAME}</at>",
"mentioned": {"id": UPN, "name": NAME}}]},
},
}],
}
for wait in (1, 2, 4, 8):
r = requests.post(WEBHOOK_URL, json=card, timeout=30)
if r.status_code != 429: # 毎秒 4 要求を超えると 429 になる
break
time.sleep(wait)
r.raise_for_status()
print(r.status_code)
Webhookのカードで指定できるのはユーザーのメンションだけで、Botへのメンションは使えません。カードの文字列ではMarkdownが使え、HTMLを書くと装飾されずに文字列のまま出ます。
28KBと毎秒4要求の上限、所有者の退職で止まるワークフローへの備え
公式ページの制限では、1メッセージは28KBまでで、超えるとエラーが返ります。1秒に4要求を超えると、その時間枠が切り替わるまで接続が絞られる仕様です。カード内でメンションする人数を増やすと、この28KBに近づいていきます。
ワークフローはチャネルではなく作成した利用者に結び付きます。作成者が退職すると、所有者のいないフローになって止まる可能性があるため注意が必要です。作成時に共同所有者を付けておく運用が要ります。フローの側でカードの組み立てや承認を作り込む方法は、Power AutomateのTeams連携とはで扱っています。
Teams SDKのBotから会話IDを保存してプロアクティブ送信する方法
Botからは、利用者の発言を待たずに通知を送れます。これをプロアクティブメッセージと呼び、Bot名義で無人の処理から送れる経路です。
インストール時に会話IDを保存してapp.sendで後から送る実装の流れ
Teams SDKのプロアクティブメッセージのページでは、送り先のチャットやチャネルの会話IDが必要で、イベントの処理中に保存しておくよう説明しています。Pythonでは次の形です。
# Bot のインストール時に会話 ID を保存し、後から通知を送る(Teams SDK for Python)
from microsoft_teams.api import InstalledActivity, MessageActivityInput
from microsoft_teams.apps import ActivityContext
storage: dict[str, str] = {} # 本番では DB などの永続ストレージにする
@app.on_install_add
async def handle_install_add(ctx: ActivityContext[InstalledActivity]):
storage[ctx.activity.from_.aad_object_id] = ctx.activity.conversation.id
await ctx.send("通知の受け取りを設定しました。")
async def notify(user_object_id: str, text: str):
conversation_id = storage.get(user_object_id)
if not conversation_id:
return # 未インストールの相手には送れない
await app.send(conversation_id, MessageActivityInput(text=text))
前提は、相手のチームや個人チャットにBotがインストールされていることです。会話IDを持たない相手には送れません。全社員に配る通知なら、管理者がアプリを一括で展開してから運用を始めます。
スレッドへの返信と特定の人にだけ見せるターゲットメッセージの扱い
同じページでは、app.reply() に会話IDと親の投稿IDを渡すと、チャネルのスレッドに返信できると説明しています。スレッドとして見えるのはチャネルだけで、1対1チャットやグループチャットでは平らに並びます。
共有の会話の中で特定の利用者にだけ見えるターゲットメッセージも送れます。プロアクティブに送る場合は、with_recipient() で受信者のアカウントを明示し、is_targeted=True を付ける指定です。ほかの参加者には表示されないので、グループチャットで個人の承認待ち件数だけを本人に知らせる、といった用途に向きます。
送信処理を本番に載せる前に決める調整制限・再送・避けたい設計
送るコードが動いても、本番で詰まるのは多くの場合、量と頻度の設計です。
1チャネル毎秒1件と1チャット毎秒1件を前提にしたキューと再送の設計
Graphの調整制限のTeamsの節では、チャネルへの投稿と返信がテナント内のアプリごとに毎秒50件、1チャネルあたり毎秒1件、1利用者あたり毎秒1件です。チャットへの送信はテナント内のアプリごとに毎秒20件、1チャットあたり毎秒1件です。記載値の約83%を長い時間窓でも評価するとの注記もあり、上限ちょうどで流し続けると429が返ります。
同じチャネルへ続けて送る処理は、キューに積んで2秒に1件程度に落とします。429を受けたら Retry-After の秒数だけ待って再送し、同じ通知が二重に出ないよう業務側のIDで送信済みかを記録しておくと安全です。
Teamsをログ置き場にする設計など採用しない送信パターンの見分け方
送信APIの注記には、Teamsをログファイルとして使うことは利用規約違反で、人が読むメッセージだけを送るようにと書かれています。監視アラートを1件ずつ全部流す、バッチの実行ログを行ごとに投稿する、といった設計はこれに当たるので採用しません。対応が必要な事象だけを、詳細画面へのリンクとともに1件にまとめて送ります。
次の3つに当てはまる場合も作り直します。サービスアカウントの委任トークンで無人の投稿を続けている、旧コネクタのURLが設定ファイルに残っている、同じ通知がWebhookとBotの両方から届いている。通知の経路が複数のシステムに散っていて整理が進まない場合は、API開発・システム連携で、送信部分の設計と実装をまとめて相談できます。
よくある質問
Teams APIでメッセージを送るときによく聞かれる質問に、公式ドキュメントの記述に沿って答えます。
Teams APIで送ったメッセージは後から編集や削除ができますか?
Graphでは、送信済みのメッセージをchatMessageの更新APIの PATCH で書き換えられます。委任アクセスなら policyViolation 以外のプロパティを更新でき、アプリケーション権限で触れるのは policyViolation だけです。必要な権限は送信とは別で、チャネルは ChannelMessage.ReadWrite、チャットは Chat.ReadWrite です。調整制限の表では、チャットのメッセージの更新と論理削除も1チャットあたり毎秒1件とされています。
メンションの相手にチャネル全体やタグを指定できますか?
この記事で扱ったGraphの作成APIの例と、WorkflowsのAdaptive Cardの例は、どちらもユーザーへのメンションです。カード書式のページには、Botのメッセージではチャネルとチームのメンションは使えないと書かれています。チャネル全体に気付かせたい場合は、チャネルの通知設定や重要度の表示に頼るより、担当者を個別にメンションする方が確実に届きます。
Teamsのメッセージに書式付きの表やコードを入れられますか?
Graphの送信では本文の contentType を html にすると、太字やリンク、箇条書きが使えます。作成APIの注記では、Teamsのクライアントには独自のHTMLの制限があり、<div> やインラインのスタイルが意図どおりに効かない場合があるという説明です。Workflowsのカードではテキストに基本的なMarkdownが使え、HTMLは文字列のまま表示されます。
Teamsではなくメールで同じ通知を送る場合はどう実装しますか?
GraphのsendMail APIを使います。メールの送信はアプリケーション権限の Mail.Send で無人の処理から送れる点が、Teamsへの投稿との大きな違いです。ただし既定ではテナント内の全メールボックスから送れる権限になるため、送信元を絞る設定が要ります。手順はMicrosoft Graph APIでメール送信する手順で解説しています。
Power Automateのフローから送る方法と、コードから送る方法はどちらが向いていますか?
送る条件と文面が固定で、業務担当者が自分で直したいならPower Automateのフローが向いています。送信の可否を業務システムのデータで細かく判定する、送信結果を記録して再送を制御する、といった処理が要るならコードから送る方が保守しやすい構成です。両方を併用するなら、フローの受け口をWorkflowsのWebhookにして、コードはURLへ送るだけにすると役割が分かれます。
関連記事
- Microsoft Graph APIとは?できること・権限設計とPythonでの呼び出し手順【2026年10月】:Graph全体の認証方式と権限の考え方
- Teams Export APIの使い方:getAllMessagesでチャットを一括エクスポートする実装手順【2026年11月】:送ったメッセージを監査用に一括で取り出す方法
- Slack Incoming Webhookの実装手順:URL発行・Block Kit整形・SDK送信と429対策:Slackで同じ通知を組む場合との比較
- Webhookの作り方と実装方法|受信側の署名検証・冪等性と送信側の再送をNode.js・Pythonで解説:送信側の再送と冪等性の作り方
- Microsoft Graph APIでメール送信する手順:sendMailの実装とMail.Send権限の絞り方【2026年11月】:Teamsではなくメールで通知する場合の実装