Microsoft Teams に自動で通知を送りたいと思い、Microsoft Graph API でプライベートチャットを操作すると、チャットは作れるのにメッセージ送信だけ「Message POST is allowed in application‑only context only for import purposes」という謎エラーで止まってしまうことがあります。本記事では、このエラーの正体と、運用環境で安全かつ現実的にメッセージを自動送信するための構成パターンをわかりやすく解説します。
Teams プライベートチャットにメッセージを送れない問題の整理
今回の前提は次のようなケースです。
- Microsoft Graph API を使って プライベートチャット(one-on-one / group) を操作したい
- Azure AD(Microsoft Entra ID)のアプリ登録で アプリケーション権限 を付与
POST /chatsでチャットの作成は成功する- しかし
POST /chats/{chat-id}/messagesで以下のエラーになる
Message POST is allowed in application‑only context only for import purposes
つまり、
- チャットは作れるのに、メッセージだけ送れない
- Graph Explorer では送れるのに、自分のアプリからだと送れない
という「モヤモヤする」状態になります。
なぜ「チャットは作れるのに、メッセージだけ送れない」のか
実は、チャットを作る API と、メッセージを送る API では、要求される権限の種類が違うためです。
| 操作 | API | 最低限の委任権限 | アプリケーション権限 |
|---|---|---|---|
| チャット作成 | POST /chats | Chat.Create など | Chat.Create(サポートあり) |
| チャットにメッセージ投稿 | POST /chats/{chat-id}/messages | ChatMessage.Send など(委任) | Teamwork.Migrate.All(移行専用) |
チャット作成 API はアプリケーション権限 Chat.Create をサポートしているため、クライアント資格情報フロー(client credentials)だけで実行できます。一方、メッセージ送信は 通常利用では委任権限しかサポートしておらず、アプリケーション権限は「履歴移行(import)」用途に限定されている、というのが根本原因です。
Graph API の権限モデルをざっくり復習
まずは、Graph API の 2 種類の権限モデルをサクッと整理します。
| 種類 | 説明 | トークンの特徴 | 主な用途 |
|---|---|---|---|
| 委任権限 (Delegated) | 「サインイン中ユーザーの代わりに」実行。ユーザーの権限の範囲内でのみ操作可能。 | scp(scope)クレームに権限名が入る。 | Web アプリ、SPA、ネイティブアプリなど、ユーザー操作を伴うシナリオ。 |
| アプリケーション権限 (Application) | アプリ自身の身元で実行。ユーザーが誰もサインインしていなくても動く。 | roles クレームに権限名が入る。 | バックエンドサービス、バッチ、監査・同期ツールなど。 |
自分のトークンがどちらかを確認する方法
- 取得済み Access Token を JWT デコーダ などでデコードする
- クレームをチェック
scpがある → 委任権限トークンrolesがある → アプリケーション権限トークン
- エラーが出ているときに
rolesしかない場合、まさに「application‑only context」で呼び出している状態です
今回のエラー文言には application-only context と書かれているので、まさに アプリケーション権限のトークンでメッセージ POST を叩いたことが示唆されています。
エラーの正体:アプリケーション権限でのメッセージ POST は「移行専用」
Microsoft の公式ドキュメントでは、チャネル/チャットのメッセージ送信について、次のように記載されています。
- API:
POST /teams/{team-id}/channels/{channel-id}/messages - API:
POST /chats/{chat-id}/messages
権限テーブルを見ると、
- 委任(work or school):
ChannelMessage.Send/ChatMessage.Send - アプリケーション:
Teamwork.Migrate.All(注記:「アプリケーション権限は migration のみ サポート」)
さらに Microsoft Q&A でも、同じエラーメッセージに対して次のような回答がついています。
- アプリケーション権限は 移行モードのチーム/チャネル に対するメッセージ移行のみに利用可能
- 通常の運用チャットにメッセージを送るには 委任権限を使う必要がある
- 1:1 チャットをアプリケーション権限だけで送ることは現在サポートされていない
つまり、エラーメッセージを直訳するとこうなります。
「メッセージの POST は、アプリケーション権限だけのコンテキストでは インポート用途のときだけ 認められます」
「インポート用途」とは、他システムから Teams にメッセージ履歴をまとめて移行するための API(Import API)を指しています。
Import(履歴移行)API のざっくり概要
Import API は、Slack や他のチャットサービスから Teams に 既存メッセージを過去日時で流し込むための仕組みです。 主な流れは次の通りです。
- 移行モードのチーム を作成
POST /teams { "@microsoft.graph.teamCreationMode": "migration", "[email protected]": "https://graph.microsoft.com/v1.0/teamsTemplates('standard')", "displayName": "移行用チーム", "createdDateTime": "2020-03-14T11:22:17.043Z" } - 移行モードのチャネル を作成
POST /teams/{team-id}/channels { "@microsoft.graph.channelCreationMode": "migration", "displayName": "履歴チャネル", "createdDateTime": "2020-03-14T11:22:17.047Z" } Teamwork.Migrate.Allアプリケーション権限でPOST /teams/{team-id}/channels/{channel-id}/messagesを呼び出し、createdDateTimeやfromを指定して「過去のメッセージ」を注入- 最後に
POST /teams/{team-id}/completeMigrationで移行モードを終了
このような「移行モード」のときだけ、アプリケーション権限でのメッセージ POST が許可されます。通常のチーム/チャネル/チャットに対して同じことをすると、まさに冒頭のエラーが返ってきます。
さらに重要なポイントとして、公式ドキュメントの Import API は チーム/チャネルのメッセージのみを対象としており、1:1 やグループチャットはスコープ外と明記されています。
| Import API のスコープ | 対象 |
|---|---|
| In-scope | Teams のチーム・チャネルメッセージ、メッセージの作成日時、インライン画像など |
| Out-of-scope | 1:1 / グループチャット、プライベートチャネル、リアクションなど |
このため、プライベートチャットに対してアプリケーション権限でメッセージを送ることは、移行 API でもサポートされていないという点に注意が必要です。
やりたいこと別:現実的な解決策の一覧
では、「運用環境で自動送信したい」という要望に対して、どのような構成を取るのがよいのでしょうか。目的別に整理すると次のようになります。
| 目的 | おすすめ構成 | 主な技術 |
|---|---|---|
| ユーザー操作をトリガーに送信(ポータルから通知、ボタン押下など) | Graph API + 委任権限 | OAuth2 / MSAL, ChatMessage.Send, Chat.ReadWrite |
| 完全自動の通知(バッチ、監視アラートなど) | Teams Bot または Incoming Webhook | Bot Framework, Teams ボット, Webhook(JSON POST) |
| 過去チャネルメッセージの一括移行 | Graph Import API(移行モード) | Teamwork.Migrate.All, @microsoft.graph.teamCreationMode など |
以下でそれぞれのパターンを具体的に見ていきます。
パターン1:ユーザー操作がある Web / ネイティブアプリ(委任権限)
社内ポータルや業務 Web アプリから、ユーザー自身の操作をトリガーに Teams にメッセージを送りたい場合は、委任権限を使った Graph API 呼び出しがもっともシンプルでサポートも厚い方法です。
必要な権限(例)
Chat.ReadWrite(委任)ChatMessage.Send(委任)- チャットを新規作成する場合は
Chat.Create(委任) - 長期的な自動処理に備えるなら
offline_accessスコープでリフレッシュトークン取得
実装フロー(ざっくり)
- Azure AD(Entra ID)にアプリ登録を作成
- API permissions で上記の委任権限を追加し、管理者同意(必要に応じて)
- アプリの種類に応じて OAuth2 フローを実装
- SPA / ネイティブアプリ:Authorization Code + PKCE
- サーバーサイド Web アプリ:Authorization Code フロー
- 取得したアクセストークン(
scpにChatMessage.Send等が入っている)を使って Graph API を呼び出す
| アプリ種別 | 推奨フロー | MSAL ライブラリ例 |
|---|---|---|
| SPA(React, Vue など) | Auth Code + PKCE | MSAL.js |
| デスクトップ / モバイル | Auth Code + PKCE / Device Code | MSAL.NET / MSAL for iOS/Android |
| サーバーサイド Web | Auth Code | MSAL.NET, MSAL Java, etc. |
委任コンテキストでの最小 C# コード例
Graph SDK v5 以降を利用したシンプルなサンプルです。ここでは、既存チャット ID が分かっている前提とします。
// 1. 事前に MSAL などでユーザーアクセストークンを取得済みとする
// scope 例: "Chat.ReadWrite ChatMessage.Send offline_access"
string accessToken = await AcquireUserAccessTokenAsync();
// 2. Graph クライアントを委任コンテキストで初期化
var graphClient = new GraphServiceClient(request =>
{
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
});
var chatId = "19:[email protected]"; // 既存チャットの ID
var message = new ChatMessage
{
Body = new ItemBody
{
Content = "こんにちは、Graph API からのメッセージです。",
ContentType = BodyType.Text
}
};
// 3. メッセージ送信
await graphClient.Chats[chatId].Messages.PostAsync(message);
この方式であれば、アプリケーション権限は一切使わずにリアルタイム送信が可能です。Graph Explorer でうまくいくのも、裏側で「委任権限のトークン」が使われているためです。
パターン2:無人バッチ・バックエンド通知(Teams Bot / Webhook)
「夜間バッチの結果を Teams に通知したい」「監視システムから障害アラートを Teams に投げたい」といった 完全自動 のシナリオでは、Graph API のアプリケーション権限だけでチャットに POST することはできません。
代わりに、次のような方法が推奨されます。
パターン2-A:Teams Bot(Bot Framework)
Teams Bot を使うと、ボット自体が「アプリケーション」として Teams に参加し、1:1 / グループチャット / チャネル に対してメッセージを送信できます。特に、プロアクティブメッセージを使うことで、ユーザー操作なしにボットから通知を飛ばすことができます。
ざっくりした構成は次の通りです。
- Azure Bot Service でボットを登録し、Teams チャネルを有効化
- Teams アプリとしてボットをパッケージングし、組織の Teams にインストール
- 必要なら Graph API で「ユーザーごとにボットを自動インストール」しておき、
chatIdを取得 - Bot Framework SDK(C#, Node.js, Python など)でプロアクティブメッセージ送信ロジックを実装
| 観点 | Teams Bot |
|---|---|
| 対応スコープ | 1:1 / グループチャット / チームチャネル |
| 完全自動送信 | 可能(プロアクティブメッセージ) |
| 実装コスト | 中〜やや高い(Bot Framework, Teams アプリ マニフェスト作成など) |
| 拡張性 | 会話型 UI、ボタン付きカード、アダプティブカードなど柔軟 |
パターン2-B:Incoming Webhook(チャネル向け)
チャネルに対する一方向通知であれば、Incoming Webhook がシンプルで扱いやすい選択肢です。
- Teams で通知したいチャネルを開き、「コネクタ」や「ワークフロー」から Incoming Webhook を作成
- Webhook URL が発行されるので、バックエンドからその URL に JSON を POST
POST https://<webhook-url> Content-Type: application/json { "text": "バッチ処理が正常終了しました。", "title": "夜間バッチ完了通知" }
制約として、
- 基本的には チャネル専用(個人チャットには送れない)
- 2025 年以降は「Workflows for Microsoft Teams」への移行が案内されているため、中長期的には別手段も検討した方がよい
といった点がありますが、「とりあえずチャネルに通知を飛ばしたい」という用途には非常に手軽です。
(補足)サービスアカウント+委任権限での半自動化
どうしても Bot を立てたくない場合、
- ライセンスを付与した「通知専用サービスユーザー」を 1 つ作成
- そのユーザーで一度サインインして委任トークンを取得
offline_accessを含めてリフレッシュトークンを保存し、バックエンドが定期的にアクセストークンに交換してメッセージ送信
という「疑似アプリケーション権限」的な構成をとることも技術的には可能です。
ただし、
- サービスユーザーにも M365 ライセンスが必要
- 「人間ユーザーのなりすまし」として扱われるため監査・セキュリティポリシー上の検討が必須
といった注意点があるので、公式に推奨されるのはボット方式と考えておくのが無難です。
パターン3:過去データ移行(Import モード)
「旧チャットシステムの履歴を Teams にアーカイブとして残したい」など、過去のメッセージをまとめて移したい場合は、前述の Import API(移行モード)を検討します。
特徴と制約
- アプリケーション権限
Teamwork.Migrate.Allが必要 - 移行対象は チーム/チャネルのメッセージ に限定
- 1:1 / グループチャットはスコープ外(サードパーティー製 migration ツールが独自ノウハウで対応しているケースが多い)
- Import 中はチーム/チャネルが「migration mode」でロックされ、ユーザーの通常操作はほとんどできない
completeMigration呼び出し後は、追加のメッセージ import ができない
| 向いているケース | 向いていないケース |
|---|---|
| 他チャットサービスから Teams への一括移行 テナント間移行でチャネル履歴も残したい | 日常運用中のチームに単発でメッセージを自動投稿したい 1:1 プライベートチャットの履歴を完全コピーしたい |
プライベートチャットの履歴移行まで求められる場合は、Graph だけでなく、専用の移行製品(Quest, AvePoint, Apps4.Pro など)の導入も検討した方が現実的です。
よくある落とし穴とその回避策
1. アプリケーション権限だけ付与している
もっとも多いのが、Azure AD アプリに アプリケーション権限だけを付与し、委任権限を追加していないパターンです。
- チャット作成(
Chat.Create)はアプリケーション権限でも動くので、「動いているように見える」 - メッセージ送信だけが「Message POST is allowed in application-only context only for import purposes」で失敗する
この場合は、
- Azure ポータルの「API permissions」で Delegated permissions に
Chat.ReadWriteChatMessage.Send
- アプリ側で ユーザーのサインインフロー(Auth Code + PKCE など)を実装し、委任トークンで Graph を呼び出す
という構成に変える必要があります。
2. トークンの中身を確認していない
「権限は追加したはずなのにエラーになる」という場合、実際に取得しているトークンのクレームを見るのが最短ルートです。
| 症状 | トークンのクレーム | 考えられる原因 |
|---|---|---|
Message POST is allowed ... | roles のみ、scp なし | client credentials で取得したアプリケーショントークンを使っている |
Forbidden / 権限不足 | scp ありだが ChatMessage.Send が含まれていない | サインイン時の要求スコープが不足している |
PreconditionFailed / application-only context not supported | roles のみ | 委任権限が必要な API をアプリケーション権限で叩いている |
JWT を一度確認するだけで、設計上の勘違いにすぐ気付けることが多いです。
3. Teams 側のポリシー・ライセンス
Graph の権限が正しくても、次の条件を満たしていないとメッセージ送信に失敗することがあります。
- 送信元ユーザーに Teams ライセンス が割り当てられていない
- Teams メッセージングポリシーで チャット機能そのものが禁止 されている
- 組織レベルで外部アプリからの投稿を制限している
とくに「個人アカウント(@outlook.com 等)では動かない」という落とし穴はよくあります。チャネルメッセージの投稿は 職場または学校アカウントのみサポートであり、個人アカウントは対象外です。
4. 新規チャット開始とメッセージ送信を混同している
Bot でも Graph API でも、「新規チャットを開始する権限」と「既存チャットに投稿する権限」は別物です。
- 新規チャット開始:
POST /chats(必要に応じてChat.Create権限) - 既存チャットに投稿:
POST /chats/{chat-id}/messages
ボットの場合、しばしば ユーザーがボットとの 1:1 チャットを少なくとも一度開くか、管理者が Graph でボットをインストールしてチャットを作成しておく必要があります。
トラブルシューティングチェックリスト(まとめ)
最後に、実際にハマりやすいポイントをチェックリスト形式で整理します。
- トークン種別を確認
- Access Token のペイロードを確認し、
scpがあるか?rolesだけではないか? rolesしかなければ、委任トークンを取得し直す。
- Access Token のペイロードを確認し、
- 権限セットを見直す
- チャット送信なら最低限:
Chat.ReadWrite+ChatMessage.Send(委任) - アプリケーション権限
Teamwork.Migrate.Allで解決しようとしていないか?(それは移行専用)
- チャット送信なら最低限:
- API バージョンとエンドポイント
- まずは
/v1.0を優先(/betaは仕様変更リスクあり) - URL が
/users/{user-id}/chats/{chat-id}/messagesなど変則的になっていないか確認
- まずは
- Teams ポリシー・ライセンス
- 送信者に Teams ライセンスが付与されているか
- メッセージングポリシーでチャットが許可されているか
- (ボットの場合)ボットがチャット/チームにインストール済みか
- Import API と通常送信 API を混同していないか
@microsoft.graph.teamCreationMode/channelCreationModeを設定した「移行モード」のチーム/チャネルだけが Import の対象- 1:1 / グループチャットの履歴移行は公式 API ではサポート外であることを理解する
- Graph Explorer との挙動差
- Graph Explorer は常に「委任権限」で動いていることを意識する
- Explorer で動くのに自分のコードで動かない場合、多くは「トークン種別の違い」が原因
まとめ:設計を切り替えればエラーは解消できる
- リアルタイム送信をしたいなら → 委任権限で Graph API を呼ぶ
- 完全自動の通知をしたいなら → Teams Bot か Webhook を使う
- アプリケーション権限でのメッセージ POST は、現状「チャネル履歴の移行専用」
今回の「チャットは作れるのにメッセージ送信だけエラーになる」問題は、Graph API の権限設計と Import API の制限を正しく理解し、構成を委任権限/ボット/Webhook のいずれかに切り替えることで解消できます。
もし運用設計として「誰の名義でメッセージを送るべきか」「どこまで自動化したいか」がまだ曖昧であれば、まずはその要件を整理し、それに最適なパターン(委任 or Bot or Import)を選ぶところから始めるのがおすすめです。

コメント