Teams Incoming Webhookでは個人チャットに送れない|2025年の正解:Teamsボット/Graph Delegatedで1:1メッセージを送信する方法と実装手順

「TeamsのIncoming Webhook経由で特定ユーザーへDMを送りたい」——多くの管理者・開発者が一度はぶつかる壁です。結論は明確で、Webhookはチャンネル専用。個人チャット(1:1)へは届きません。本記事では、その理由と2025年時点の正解(Teamsボット/Microsoft GraphのDelegated)を、実装手順・コード例・運用設計まで一気通貫で解説します。

目次

Teams Incoming Webhook では個人チャットに送れない理由と正解ルート

Incoming Webhook は Teams の「チャンネル」に紐づくコネクタであり、投稿先のスコープは常にチャネル会話です。個人チャット(1:1)にはスコープ外のため送信できません。ここを無理に迂回しようとしても、HTTP 200 が返っても相手の1:1には届かない、という結果になります。

送信先Incoming Webhook可否代替手段備考
Teams チャンネル可Incoming Webhook / Bot / Graphチームの任意チャネルに投稿可能
個人チャット(1:1)不可Teamsボット(プロアクティブ) / Graph(Delegated)Webhookはスコープ外
グループチャット(複数人)不可ボット / Graph(Delegated)Graphはチャット作成後に投稿

個人宛て通知の正攻法は次の2つです。

  • Teams ボット(Azure Bot Service + Bot Framework + Teams SDK)でプロアクティブ メッセージを送る。
  • Microsoft Graph API(Delegated)で、サインイン済みユーザーの権限でチャットを作成・投稿する。

Graph API で 1:1 を送るときの基本(Delegated が必須)

Graph では /chats/{chat-id}/messages に POST することでチャットへ投稿できます。ただし 1:1 送信はユーザー委任(Delegated)のアクセストークンが必須です。アプリのみ(App-only)トークンでは 1:1 の通常投稿はブロックされ、「外部インポート用途」に限定されます。

項目要点
必要スコープChat.ReadWrite または Chat.ReadWrite.All(Delegated)
できないことApp-only での 1:1 通常投稿(インポート用途を除く)
基本手順① ユーザーでサインインしトークン取得 → ② 相手ユーザーIDを取得 → ③ 1:1チャットを取得 or 作成(chatType=oneOnOne)→ ④ /chats/{chat-id}/messages へ POST

PowerShell(Invoke‑RestMethod)による最小実装の流れ(検証用)

以下は概念実証(PoC)向けの最短ルートです。運用では安全なフロー(デバイスコード/認可コード)への置き換えを推奨します。

$tenantId  = "<tenant-id>"
$clientId  = "<public-client-id>" # モバイル/デスクトップ向けパブリッククライアント
$username  = "<user-upn>"
$password  = "<password>"         # ROPCは非推奨。検証用途限定。

# 1) ユーザートークン取得(ROPC例:非推奨)

$tokenBody = @{
client_id  = $clientId
scope      = "[https://graph.microsoft.com/.default](https://graph.microsoft.com/.default) offline_access openid profile"
grant_type = "password"
username   = $username
password   = $password
}
$token = Invoke-RestMethod -Method Post `  -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"`
-Body $tokenBody
$accessToken = $token.access_token

# 2) 相手ユーザーを取得(メール/UPNからIDへ)

$targetUser = Invoke-RestMethod -Headers @{Authorization = "Bearer $accessToken"} `
-Uri "[https://graph.microsoft.com/v1.0/users/<target-user-upn-or-mail](https://graph.microsoft.com/v1.0/users/<target-user-upn-or-mail)>"
$targetUserId = $targetUser.id

# 3) 1:1チャットを作成(存在しなければ生成)

$chatCreateBody = @{
chatType = "oneOnOne"
members = @(
@{
"@odata.type" = "#microsoft.graph.aadUserConversationMember"
"roles"       = @("owner")
"[[email protected]](mailto:[email protected])" = "[https://graph.microsoft.com/v1.0/me](https://graph.microsoft.com/v1.0/me)"
},
@{
"@odata.type" = "#microsoft.graph.aadUserConversationMember"
"roles"       = @("owner")
"[[email protected]](mailto:[email protected])" = "[https://graph.microsoft.com/v1.0/users('$targetUserId](https://graph.microsoft.com/v1.0/users%28'$targetUserId)')"
}
)
} | ConvertTo-Json -Depth 5

$chat = Invoke-RestMethod -Method Post -Headers @{Authorization="Bearer $accessToken";"Content-Type"="application/json"} `
-Uri "[https://graph.microsoft.com/v1.0/chats](https://graph.microsoft.com/v1.0/chats)" -Body $chatCreateBody
$chatId = $chat.id

# 4) メッセージを送信(HTML可)

$msgBody = @{
body = @{
contentType = "html"
content = "自動通知です。
詳細はポータルを確認してください。" } } | ConvertTo-Json Invoke-RestMethod -Method Post -Headers @{Authorization="Bearer $accessToken";"Content-Type"="application/json"} ` -Uri "[https://graph.microsoft.com/v1.0/chats/$chatId/messages](https://graph.microsoft.com/v1.0/chats/$chatId/messages)" -Body $msgBody

注意: ROPC はセキュリティとポリシー面の制約が大きく、実運用ではデバイスコードや認可コード+PKCE等へ移行してください。

cURL(Delegatedトークンがある前提)

# チャット作成
curl -X POST https://graph.microsoft.com/v1.0/chats \
 -H "Authorization: Bearer <access_token>" \
 -H "Content-Type: application/json" \
 -d '{
  "chatType":"oneOnOne",
  "members":[
    {"@odata.type":"#microsoft.graph.aadUserConversationMember","roles":["owner"],"[email protected]":"https://graph.microsoft.com/v1.0/me"},
    {"@odata.type":"#microsoft.graph.aadUserConversationMember","roles":["owner"],"[email protected]":"https://graph.microsoft.com/v1.0/users('\''<target-user-id>'\'' )"}
  ]
}'

# メッセージ送信

curl -X POST [https://graph.microsoft.com/v1.0/chats/<chat-id>/messages](https://graph.microsoft.com/v1.0/chats/<chat-id>/messages) 
-H "Authorization: Bearer " 
-H "Content-Type: application/json" 
-d '{"body":{"contentType":"html","content":"こんにちは"}}' 

よくあるエラーと対処

エラー例原因対処
401 Unauthorizedトークン不正、スコープ不足Delegated で Chat.ReadWrite を付与して再発行
Requested API is not supported in application‑only contextApp-only トークンで呼び出しDelegated に切替、またはボットへ設計変更
403 Forbidden組織ポリシー/ライセンス/レプリカ遅延Teams ライセンス、メッセージングポリシー、外部アクセス設定を確認
404 Not Found(チャットID関連)チャット未作成/IDミスPOST /chats で 1:1 を作成してから送信
400 Bad Request(HTML不正等)サポート外タグ・本文サイズ超過プレーンテキストで検証、本文を短く/改行を整理

「メッセージ送信」と「返信」の違い(個人チャットは返信APIなし)

操作現状の可否備考
1:1チャットに新規メッセージ送信可能(Delegated)/chats/{chat-id}/messages に POST
既存メッセージにスレッド返信未対応(個人チャット)返信APIはチャンネル会話用に限定

Teams ボットでのプロアクティブ メッセージが最も確実

サーバー側からユーザーの操作なしに 1:1 を確実に送るなら、Teams ボットが王道です。ポイントは「ボットがユーザーの個人スコープにインストールされている」ことと、「会話リファレンス(ConversationReference)を保持」することです。

ボットの標準フロー

  1. Azure Bot Service でボット登録(チャネルに Teams を追加)。
  2. ユーザーまたは管理者がボットを個人スコープにインストール(アプリ配布/自動配置ポリシー)。
  3. 初回起動やインストールイベントで ConversationReference を保管(DB 等)。
  4. バックエンドから任意タイミングで proactive に送信。

C#(Bot Framework v4)概略コード

public class ProactiveSender
{
    private readonly BotFrameworkAdapter _adapter;
    private readonly string _appId;      // Bot AppId
    private readonly string _serviceUrl; // 既知の serviceUrl(会話から取得して保存)
    public ProactiveSender(BotFrameworkAdapter adapter, string appId, string serviceUrl)
    {
        _adapter = adapter; _appId = appId; _serviceUrl = serviceUrl;
    }


public async Task SendToUserAsync(string tenantId, string userAadObjectId, string message, CancellationToken ct)
{
    var user = new ChannelAccount(id: userAadObjectId);
    var bot  = new ChannelAccount(id: _appId);

    var parameters = new ConversationParameters
    {
        IsGroup = false,
        Bot = bot,
        Members = new List<ChannelAccount> { user },
        TenantId = tenantId
    };

    await _adapter.CreateConversationAsync(
        channelId: "msteams",
        serviceUrl: _serviceUrl,
        credentials: null,
        parameters: parameters,
        callback: async (turnContext, cancellationToken) =>
        {
            await turnContext.SendActivityAsync(MessageFactory.Text(message), cancellationToken);
        },
        cancellationToken: ct);
}


} 

実運用では、ユーザーごとに保存した ConversationReference を使って ContinueConversationAsync で送るパターンが安定します。インストールされていないユーザーには送れないため、配布ポリシーや事前インストールの設計が肝です。

2025年時点のベストプラクティスと選定表

目的推奨アプローチ理由 / 注意点
サーバーから自動通知(ユーザーログイン不要)Teams ボット(プロアクティブ)1:1宛てを公式に・確実に送れる。インストール必須。料金・審査考慮。
サインイン済みクライアントから送信Graph API(Delegated)UIから自然に送れる。Chat.ReadWrite スコープが必要。
バックグラウンドApp-onlyで1:1送信現状不可通常投稿は不可。インポート用途に限定。要件次第でボットへ転換。

必要権限と代表エンドポイントの整理

シナリオ主な権限/スコープエンドポイント例
Teams ボットから 1:1ボット登録時の権限(ユーザー/チャットの読み書き)SDK内部(CreateConversationAsync → SendActivityAsync)
Graph(Delegated)Chat.ReadWrite, Chat.ReadWrite.AllGET /users/{id}
POST /chats(chatType=oneOnOne)
POST /chats/{chat-id}/messages
Graph(App-only)ChatMessage.Send など(管理者同意)/chats/{chat-id}/messages(インポート用途)

実装ディテール:Graph Delegated を使う最短ルート

ステップ1:ユーザーでサインインしてトークンを取る

  • 本番は「認可コード + PKCE」または「デバイスコード」推奨。
  • 検証では ROPC を使えるが、条件付きアクセスやMFAに阻まれる場合が多い。

ステップ2:相手のユーザーID(Entra ID オブジェクトID)を取得

GET /v1.0/users/{mail-or-upn}
→ レスポンスの id を使用

ステップ3:1:1チャットを作成 or 既存を利用

既存検索は GET /me/chats?$filter=chatType eq 'oneOnOne' を列挙し、メンバーに対象IDが含まれるかで判定。なければ POST /chats で作成します。

ステップ4:メッセージ送信

POST /v1.0/chats/{chat-id}/messages
{
  "body": {
    "contentType": "html",
    "content": "通知本文 &lt;br/&gt; 詳細はポータルで確認してください。"
  }
}

Teams ボット設計の要点(プロアクティブ)

  • インストール要件:ユーザーの個人スコープにボットが入っていないとDM不可。
  • 会話リファレンスの保存:ユーザーごとに最新の serviceUrl/conversation を安全に保管。
  • 多言語・リッチカード:Adaptive Cardsで配信可。既読は Signal ではなくイベント/Graph で取得検討。
  • 配布:テナント内配布、ストア審査、App Setup Policy による自動配布など運用決定。

Webhook / Graph / ボットの比較(意思決定の早見表)

観点Incoming WebhookGraph(Delegated)Teams ボット
送信先チャンネルのみ1:1 / グループ / チャンネル(要権限)1:1 / グループ / チャンネル
ユーザーログイン不要可不可(Delegated必須)可
公式サポートの確実性高(チャンネル限定)高(要件に合致すれば)最高(DM通知の王道)
実装の難易度低中中〜高
拡張性(カード/アクション)低中高

運用で効くチェックリスト

  • 配信保証:再送・リトライ(429/5xx)と冪等キーを設計する。
  • 到達可視化:配信ログ、失敗の分類(ポリシー・無インストール・無権限・容量超過)。
  • レート制御:バーストを避け、キューで平滑化。チャット作成はキャッシュ。
  • 本文制限:メッセージ長、HTMLタグ制限、添付/メンション数を上限内に。
  • セキュリティ:最小権限、シークレット運用、監査ログ、退職者配信の抑止。
  • コンプライアンス:保持/監査/情報保護ラベルの方針に沿う。

トラブルシューティング深掘り

「送れたり送れなかったり」問題

  • ボットが未インストールのユーザーは失敗。自動配布ポリシーを活用。
  • Graph Delegated はユーザー側の Teams ポリシーの影響を受ける。
  • 相手が外部組織の場合、外部アクセス設定やフェデレーションの影響がある。

メンション・カードが崩れる

  • メンションは <at id="0">太郎</at> 形式と mentions 配列を一致させる。
  • Adaptive Cards はバージョン差異に注意。プレーンテキストでまず通るか確認。

監査・可観測性

  • メッセージID、チャットID、ユーザーID、コリレーションIDをログ化。
  • SLI/SLO を定義(到達率、遅延、リトライ率)。

サンプル:Python(requests)で 1:1 を送る

import requests

access_token = ""
target_user_id = ""

# チャット作成

chat_payload = {
"chatType": "oneOnOne",
"members": [
{"@odata.type":"#microsoft.graph.aadUserConversationMember","roles":["owner"],"[[email protected]](mailto:[email protected])":"[https://graph.microsoft.com/v1.0/me](https://graph.microsoft.com/v1.0/me)"},
{"@odata.type":"#microsoft.graph.aadUserConversationMember","roles":["owner"],"[[email protected]](mailto:[email protected])":f"[https://graph.microsoft.com/v1.0/users('{target_user_id}')](https://graph.microsoft.com/v1.0/users%28'{target_user_id}'%29)"}
]
}
r = requests.post("[https://graph.microsoft.com/v1.0/chats](https://graph.microsoft.com/v1.0/chats)",
headers={"Authorization": f"Bearer {access_token}", "Content-Type":"application/json"},
json=chat_payload)
r.raise_for_status()
chat_id = r.json()["id"]

# メッセージ送信

msg = {"body":{"contentType":"html","content":"Pythonからの自動通知です。"}}
r = requests.post(f"[https://graph.microsoft.com/v1.0/chats/{chat_id}/messages](https://graph.microsoft.com/v1.0/chats/{chat_id}/messages)",
headers={"Authorization": f"Bearer {access_token}", "Content-Type":"application/json"},
json=msg)
r.raise_for_status() 

サンプル:C#(Graph SDK, Delegated)

using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;
using Azure.Identity;

// 対話式(デバイスコード等)でトークンを取得
var scopes = new[] { "Chat.ReadWrite" };
var options = new DeviceCodeCredentialOptions { ClientId = "", TenantId = "" };
var credential = new DeviceCodeCredential(o => { Console.WriteLine(o.Message); return Task.CompletedTask; }, options);
var authProvider = new TokenCredentialAuthenticationProvider(credential, scopes);
var graph = new GraphServiceClient(authProvider);

// 相手ユーザー
var user = await graph.Users[""].GetAsync();

// 1:1チャット作成
var chat = await graph.Chats.PostAsync(new Chat
{
ChatType = ChatType.OneOnOne,
Members = new List
{
new AadUserConversationMember { Roles = new List { "owner" }, AdditionalData = new Dictionary { ["[[email protected]](mailto:[email protected])"] = "[https://graph.microsoft.com/v1.0/me](https://graph.microsoft.com/v1.0/me)" } },
new AadUserConversationMember { Roles = new List { "owner" }, AdditionalData = new Dictionary { ["[[email protected]](mailto:[email protected])"] = $"[https://graph.microsoft.com/v1.0/users('{user.Id}](https://graph.microsoft.com/v1.0/users%28'{user.Id})')" } }
}
});

// メッセージ
await graph.Chats[chat.Id].Messages.PostAsync(new ChatMessage
{
Body = new ItemBody { ContentType = BodyType.Html, Content = "C# Graph SDK からの通知" }
}); 

セキュリティ/ガバナンス上の留意点

  • 最小権限:不要なスコープは付与しない。管理者同意の審査をクリアする。
  • 秘密情報の管理:キー/証明書のローテーション、キーバックアップ、監査。
  • ユーザー保護:配信頻度・時間帯を制御し、迷惑通知化を避ける。
  • コンプライアンス:保持/監査/データ主権に準拠。PIIの扱いに注意。

FAQ(実案件でよく聞かれること)

  • Q: Webhook の URL を個人チャットに向け替えられませんか?
    A: できません。Webhook はチャンネルのリソースに結びついています。
  • Q: App-only でどうにか 1:1 を送りたい。
    A: 通常投稿は不可です。ボットでの配信に設計を切り替えるのが現実解です。
  • Q: 返信(スレッド)で送りたい。
    A: 1:1 では返信APIがありません。メッセージとして新規投稿してください。
  • Q: 既存チャットが大量にあると検索が重い。
    A: 「必要なら作成」方針で POST /chats を先に打ち、ID をキャッシュすると軽くなります。

まとめ:設計の指針

  • Webhookでは1:1に送れない——ここは仕様として割り切る。
  • サーバー主導のDM通知はボットが最適解(プロアクティブ)。
  • クライアント発の送信はGraph Delegatedが簡潔で扱いやすい。
  • App-only での 1:1 は現状方針外。要件とセキュリティを踏まえて構成を選ぶ。
  • 運用はレート制御・配信保証・監査まで設計して初めて安定稼働。

付録:コピーして使える PowerShell スニペット集

Access Token(デバイスコード例:ユーザーにブラウザ表示)

# MSAL.PS を使う例(事前に Install-Module MSAL.PS)
$token = Get-MsalToken -ClientId "&lt;client-id&gt;" -TenantId "&lt;tenant-id&gt;" -Scopes "Chat.ReadWrite" -DeviceCode
$accessToken = $token.AccessToken

1:1 チャット作成 → 送信

$headers = @{ Authorization = "Bearer $accessToken"; "Content-Type"="application/json" }

# ユーザーIDを UPN から取得

$user = Invoke-RestMethod -Headers $headers -Uri "[https://graph.microsoft.com/v1.0/users/<target-upn](https://graph.microsoft.com/v1.0/users/<target-upn)>"

# 作成

$body = @{
chatType = "oneOnOne"
members = @(
@{ "@odata.type"="#microsoft.graph.aadUserConversationMember"; roles=@("owner"); "[[email protected]](mailto:[email protected])"="[https://graph.microsoft.com/v1.0/me](https://graph.microsoft.com/v1.0/me)" },
@{ "@odata.type"="#microsoft.graph.aadUserConversationMember"; roles=@("owner"); "[[email protected]](mailto:[email protected])"="[https://graph.microsoft.com/v1.0/users('$($user.id)](https://graph.microsoft.com/v1.0/users%28'$%28$user.id%29)')" }
)
} | ConvertTo-Json -Depth 5

$chat = Invoke-RestMethod -Method Post -Headers $headers -Uri "[https://graph.microsoft.com/v1.0/chats](https://graph.microsoft.com/v1.0/chats)" -Body $body

# 送信

$msg = @{ body = @{ contentType="html"; content="PowerShell からの通知"; } } | ConvertTo-Json
Invoke-RestMethod -Method Post -Headers $headers -Uri "[https://graph.microsoft.com/v1.0/chats/$($chat.id)/messages](https://graph.microsoft.com/v1.0/chats/$%28$chat.id%29/messages)" -Body $msg 

最後に

仕様は変化が速く、プラットフォームの制限やポリシーも更新されます。実装前に必ず最新のドキュメントとテナント側のポリシーを点検し、PoC→段階的展開→本番化の順でリスクを抑えていきましょう。個人宛て通知は「ボットが王道、Graph Delegatedが堅実」、この方針で設計すれば迷いません。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次