Teams Graph APIで会議トランスクリプトが取得できない原因と解決策|400 BadRequest「onlineMeetings が見つからない」を確実に直す方法

Teams 会議のトランスクリプトを Graph API で取得しようとして「Resource not found for the segment ‘onlineMeetings’」に詰まる――この“あるある”を、原因切り分けから正しいエンドポイント、権限、実装の落とし穴まで一気に解消します。現場運用でハマりやすいパターンを表とサンプルコードで具体化し、再現手順と検証チェックリストもまとめました。

目次

症状

次のように呼び出すと 400 BadRequest で Resource not found for the segment ‘onlineMeetings’ が返ります。

GET https://graph.microsoft.com/v1.0/onlineMeetings/{meeting_id}/transcripts
Authorization: Bearer <access_token>

これは URL セグメントの誤りが主因です。正しいパスは /communications/onlineMeetings または /users/{userId}/onlineMeetings の配下にあります。

主な原因(要点まとめ)

原因詳細
エンドポイントの誤りv1.0 では /communications/onlineMeetings/{onlineMeetingId}/transcripts または /users/{userId}/onlineMeetings/{onlineMeetingId}/transcripts を使用する。
トランスクリプト未生成会議中に「文字起こしを開始」していないと、終了後 API で取得できるデータは存在しない(空配列)。
サポート外の会議形式プライベート チャネル会議や、API だけで作成した即席(アドホック)会議は現行 v1.0 の Transcript API 非対応。
権限不足最低でも OnlineMeetingTranscript.Read.All が必要。アプリケーション権限の場合はアプリケーション アクセス ポリシーの付与も必須。

正しいエンドポイントの選び方

利用する認可方式とユースケースで URL を使い分けます。

用途推奨エンドポイント備考
バックエンドサービスで組織横断に取得GET /v1.0/communications/onlineMeetings/{onlineMeetingId}/transcriptsアプリケーション権限での利用に向く。アプリケーション アクセス ポリシーで対象ユーザーを明示する。
ユーザー署名で自分の会議を取得GET /v1.0/users/{userId}/onlineMeetings/{onlineMeetingId}/transcripts委任権限での利用。{userId} は UPN も可。

URL 例(v1.0)

GET https://graph.microsoft.com/v1.0/communications/onlineMeetings/{onlineMeetingId}/transcripts
GET https://graph.microsoft.com/v1.0/users/{userId}/onlineMeetings/{onlineMeetingId}/transcripts

正しい onlineMeetingId を用意する

  • UI の「会議 ID(数字コード)」や「会議パスコード」は Graph の onlineMeetingId ではありません。
  • 確実なのは、会議の joinWebUrl(招待リンク)からオンライン会議リソースを取得して id を得る手順です。
# 例:ユーザー特定済みで Join URL から会議を引く(委任)
GET /v1.0/users/{userId}/onlineMeetings?$filter=JoinWebUrl eq '{joinWebUrl}'

上記の応答で得た value[0].id が {onlineMeetingId} です。これを transcripts のパスに差し込みます。

トランスクリプトが存在する条件

  • 会議中に主催者または許可された参加者が「… → 文字起こしを開始」を実行していること。
  • 会議ポリシーでトランスクリプト(自動文字起こし)が許可されていること。
  • API から取得できるのは会議終了後のファイル(VTT/JSON メタデータ)。リアルタイム取得は不可。

満たしていない場合、API 応答は {"value": []} となります。

サポート外の会議形式と落とし穴

会議タイプトランスクリプト API補足
通常の予定表付き Teams 会議対応最も安定。主催者・出席者のポリシー要件に注意。
プライベート チャネル会議非対応チャネルのスコープ仕様により取得対象外。
API だけで作成した即席(アドホック)会議非対応create onlineMeeting で生成しただけの会議はトランスクリプト対象外。

必要な権限と管理者設定

最低限必要な Graph 権限

権限名種類用途
OnlineMeetingTranscript.Read.All委任 / アプリケーショントランスクリプトの取得に必須。
OnlineMeetings.Read.All委任 / アプリケーションオンライン会議本体を探索・照会する際に推奨。

アプリケーション アクセス ポリシー(アプリ権限時)

アプリケーション権限で /communications/ を使う場合、Teams 管理でアプリのアクセス範囲(対象ユーザー)を明示します。代表的な PowerShell 手順は以下の通りです。

# Teams PowerShell に接続(管理者で)
Connect-MicrosoftTeams

# アプリ登録(AppId)は Azure AD で作成済とする

$AppId = ""

# ポリシー作成(初回のみ)

New-CsApplicationAccessPolicy -Identity "GraphTranscriptPolicy" -AppIds $AppId

# アクセスさせたいユーザーに付与(個別 or グループ)

Grant-CsApplicationAccessPolicy -PolicyName "GraphTranscriptPolicy" -Identity [[email protected]](mailto:[email protected])
Grant-CsApplicationAccessPolicy -PolicyName "GraphTranscriptPolicy" -Identity [[email protected]](mailto:[email protected])

# 反映確認

Get-CsApplicationAccessPolicy -Identity "GraphTranscriptPolicy"

あわせて Azure AD 側で当該権限に対し「管理者の同意」を与えておきます。

実装スニペット(Python / Node / PowerShell)

Python(requests)

import requests

access_token = "YOUR_ACCESS_TOKEN"
meeting_id = "YOUR_MEETING_ID"  # Graph の onlineMeeting.id

url = f"[https://graph.microsoft.com/v1.0/communications/onlineMeetings/{meeting_id}/transcripts](https://graph.microsoft.com/v1.0/communications/onlineMeetings/{meeting_id}/transcripts)"
headers = {"Authorization": f"Bearer {access_token}"}

resp = requests.get(url, headers=headers)
resp.raise_for_status()

for item in resp.json().get("value", []):
print("Transcript VTT URL:", item.get("contentUrl"))
print("Metadata URL:", item.get("metadataContentUrl"))

Node.js(@microsoft/microsoft-graph-client)

import { Client } from "@microsoft/microsoft-graph-client";
import "isomorphic-fetch";

const client = Client.init({
authProvider: (done) => done(null, process.env.ACCESS_TOKEN)
});

const meetingId = process.env.MEETING_ID; // onlineMeeting.id
const path = `/communications/onlineMeetings/${meetingId}/transcripts`;

const res = await client.api(`/v1.0${path}`).get();
for (const t of res.value ?? []) {
console.log("VTT:", t.contentUrl);
}

PowerShell(Microsoft Graph PowerShell SDK)

# 権限に管理者同意済みで接続
Connect-MgGraph -Scopes "OnlineMeetingTranscript.Read.All"

$meetingId = ""
$transcripts = Invoke-MgGraphRequest -Method GET -Uri "[https://graph.microsoft.com/v1.0/communications/onlineMeetings/$meetingId/transcripts](https://graph.microsoft.com/v1.0/communications/onlineMeetings/$meetingId/transcripts)"

$transcripts.value | ForEach-Object {
Write-Host "VTT:" $_.contentUrl
}

cURL(快速検証)

curl -sS -H "Authorization: Bearer <TOKEN>" \
  "https://graph.microsoft.com/v1.0/communications/onlineMeetings/<onlineMeetingId>/transcripts"

レスポンス構造とダウンロード

一覧の応答は概ね次のような JSON です。

{
  "value": [
    {
      "id": "a1b2c3...",
      "createdDateTime": "2025-05-01T03:12:45Z",
      "meetingId": "MSpkZWY...(省略)",
      "contentUrl": "https://<短期有効なURL>/transcript.vtt",
      "metadataContentUrl": "https://<短期有効なURL>/metadata.json",
      "state": "transcribed"
    }
  ]
}
  • contentUrl は短期有効なダウンロード URL(期限切れの場合は一覧を再取得して最新 URL を入手)。
  • metadataContentUrl は話者やタイムスタンプ、言語などのメタ情報。

失敗しないためのポイント

URL をハードコードしない

ベースは https://graph.microsoft.com/v1.0、サービスパスは /communications/onlineMeetings/{id}/transcripts か /users/{userId}/onlineMeetings/{id}/transcripts。バージョンやセグメントは定数化し、誤入力を防ぎます。

会議の同定を厳密に

  • 招待リンク(Join URL)から会議を検索して id を取得するルートを一本化。
  • 日時や件名でのあいまい検索は避ける(重複やタイムゾーン差異で誤同定しやすい)。

ポリシーの事前整備

  • 主催者の会議ポリシーで「トランスクリプト許可」を有効に。
  • アプリ利用の場合はアプリケーション アクセス ポリシーを事前配布(新入社員の入社日に自動付与する運用が安定)。

よくあるエラーと対処

HTTPメッセージ例原因対処
400Resource not found for the segment ‘onlineMeetings’URL セグメント誤り(/communications or /users/{userId} がない)正しいパスに修正。
401InvalidAuthenticationTokenトークン失効/スコープ不足トークン更新、必要スコープを付与。
403AccessDenied権限不足、またはアプリケーション アクセス ポリシー未付与権限の管理者同意とポリシー付与を確認。
404ItemNotFound会議が見つからない、またはトランスクリプト未生成Join URL から会議 ID を再取得/会議中に文字起こしを開始する。
409Conflict会議リソースの状態競合数秒待ってリトライ(指数バックオフ)。
429Too Many RequestsスロットリングRetry-After を尊重しバックオフ。
5xxServiceUnavailable など一時的障害冪等な再試行、サーキットブレーカ設計。

検証チェックリスト(現場向け)

  1. テナントでトランスクリプト許可ポリシーが有効か。
  2. 対象会議で実際に「文字起こしを開始」したか(テスト会議で再現)。
  3. Join URL から onlineMeeting.id を取得できたか。
  4. アプリの権限(OnlineMeetingTranscript.Read.All)に管理者同意済みか。
  5. アプリケーション アクセス ポリシーで対象ユーザーにアプリを付与済みか。
  6. 正しいエンドポイント(/communications or /users/{userId})を使っているか。
  7. 応答の contentUrl が期限切れなら再取得しているか。

VTT を SRT に変換する(Python 例)

取得した contentUrl から VTT をダウンロードしたら、字幕用途に SRT へ変換できます。

from datetime import datetime

def vtt_time_to_srt(t):
# 00:00:01.234 --> 00:00:01,234
return t.replace('.', ',')

def vtt_to_srt(vtt_text):
lines = [l.rstrip() for l in vtt_text.splitlines()]
out, idx = [], 1
buf = []
for line in lines:
if line.startswith("WEBVTT") or line.startswith("Kind:") or line.startswith("Language:"):
continue
if not line:
if buf:
out.append(str(idx))
out.extend(buf)
out.append("")
idx += 1
buf = []
continue
if "-->" in line:
start, end = [p.strip() for p in line.split("-->")]
buf.append(f"{vtt_time_to_srt(start)} --> {vtt_time_to_srt(end)}")
else:
buf.append(line)
return "\n".join(out)

# 使い方:vtt_text = open("transcript.vtt", "r", encoding="utf-8").read()

# srt_text = vtt_to_srt(vtt_text)

運用に効くサンプル設計

バッチ収集(毎朝の監査用サマリ)

  • 対象ユーザーの前日会議を列挙 → onlineMeeting.id を解決 → transcripts 一覧 → VTT/JSON をストレージへ保存。
  • VTT は SRT 変換後にメディア資産と一緒に格納、メタデータ JSON は検索インデックスへ投入。
  • 期限付き URL を直接保管せず、常に API で再解決する。

Web アプリ(会議サマリ表示)

  • ダッシュボードで会議ごとに「トランスクリプトあり/なし」を明示。
  • ダウンロードはバックエンドで代理取得し、社内ストレージの一時 URL を払い出す。
  • 発話者ラベルはメタデータ JSON を優先してマッピング(表示名変更に強い)。

リアルタイムが必要な場合の選択肢

Graph の Transcripts API はリアルタイム取得に非対応です。会議中の字幕や要約が必要なら、Graph Communications Calling SDK 等で音声ストリームを取得し、外部の音声認識エンジンで同時転写するアーキテクチャを採用します。録画・保存はコンプライアンス要件に従い、同意と保護(暗号化、アクセス制御)を徹底します。

トラブル対応のフロー(簡易)

ステップ確認事項YesNo
1URL は /communications or /users/{userId}/ か2 へURL を修正
2onlineMeeting.id を Join URL から取得したか3 へJoin URL から再解決
3会議中に文字起こしを開始していたか4 へテスト会議で開始 → 再試行
4権限とポリシー(管理者同意/アクセス ポリシー)は整備済みか5 へ権限とポリシーを付与
5contentUrl の期限切れを考慮しているか完了一覧を再取得して最新 URL を使用

完全版サンプル:Join URL → transcript VTT 取得(Python)

Join URL から会議 ID を取得 → transcripts を列挙 → VTT を保存、までを一気通貫で。

import requests, sys, os, json

GRAPH = "[https://graph.microsoft.com/v1.0](https://graph.microsoft.com/v1.0)"
TOKEN = os.environ["ACCESS_TOKEN"]
USER  = os.environ.get("USER_ID")  # 委任なら "me" でも可
JOIN  = os.environ["JOIN_WEB_URL"]

def gget(path, params=None):
r = requests.get(GRAPH+path, headers={"Authorization": f"Bearer {TOKEN}"}, params=params)
r.raise_for_status()
return r.json()

# 1) Join URL から onlineMeeting.id を取得

meetings = gget(f"/users/{USER}/onlineMeetings", params={"$filter": f"JoinWebUrl eq '{JOIN}'"})
if not meetings.get("value"):
sys.exit("meeting not found")
mid = meetings["value"][0]["id"]

# 2) transcripts を列挙

ts = gget(f"/communications/onlineMeetings/{mid}/transcripts")
if not ts.get("value"):
sys.exit("no transcript")

# 3) VTT を保存(contentUrl は短期有効)

vtt_url = ts["value"][0]["contentUrl"]
vtt = requests.get(vtt_url).text
open("transcript.vtt","w", encoding="utf-8").write(vtt)
print("saved transcript.vtt")

セキュリティとコンプライアンスの注意

  • 字幕データは個人情報や機密を含み得ます。保存先は暗号化ストレージ、アクセスは最小権限で。
  • 期限付き URL を公開チャネルやログに残さない(短期でも漏洩リスク)。
  • 二次利用(要約・翻訳)時は情報分類に応じたマスキング・自動失効を実装。

「これだけやれば直る」最短手順

  1. 会議中に一度「文字起こしを開始」して終了まで進める。
  2. Azure AD でアプリに OnlineMeetingTranscript.Read.All を付与し、管理者同意。
  3. Teams PowerShell でアプリケーション アクセス ポリシーを作成・対象ユーザーに付与。
  4. GET /users/{userId}/onlineMeetings?$filter=JoinWebUrl eq '{joinWebUrl}' で id を取得。
  5. GET /communications/onlineMeetings/{id}/transcripts で一覧 → contentUrl から VTT ダウンロード。

これで「BadRequest」エラーを解消し、狙った会議のトランスクリプトを取得できます。

この記事を書いた人

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

コメント

コメントする

目次