業務システム

Teams APIでメッセージ送信する方法:Graph・Workflows・Botの実装とメンション・カードのコード例【2026年11月】

Microsoft dynamics 365導入支援の手順

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の公式ページの手順は次のとおりです。

  1. 投稿先のチャネルの「…」から「Workflows」を開く
  2. 「Send webhook alerts to a channel」などのテンプレートを選ぶ
  3. 投稿先などのパラメーターを設定して保存する
  4. 作成されたワークフローの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へ送るだけにすると役割が分かれます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.05 テックブログ Linuxカーネルの脆弱性CVE-2026-98432:修正版の見分け方とnftablesの緩和策
  2. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方
  3. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  4. 2026.11.05 テックブログ SMBCコンシューマーファイナンスの情報漏洩:最大32万人の対象確認と保証会社に集まる情報
  5. 2026.11.06 テックブログ ドコモ不正アクセス:最大29.5万件の流出項目と見舞金、My docomo停止の経過と偽連絡の見分け方

RELATED POSTS 関連記事

目次