Microsoft Graph People APIのidが/people/{id}で取得できない理由と正しい対処法(getByIdsによる型解決とOutlook Peopleとの違いを解説)

Microsoft Graph の People API を使うと、Outlook の「People(連絡先)」や最近やり取りした相手をまとめて検索できて便利な一方で、「/people で返ってきた id を /people/{id} に渡すと 404 になる」「Outlook には表示されているのに /people?$search では見つからない」といったハマりどころが多くあります。この記事では、その原因と根本的な対処法、実務で安定して動く実装パターンを詳しく解説します。

目次

この記事で解決すること

本記事では、以下のような疑問に答えます。

  • GET /v1.0/users/{user-id}/people?$search="benjamin" で見つかった人物の id を、GET /v1.0/users/{user-id}/people/{person-id} に渡しても取得できないのはなぜか?
  • Outlook の「People(連絡先)」画面には表示される相手が、/people?$search= ではヒットしないのはなぜか?
  • People API 経由で人物を POST / PATCH / DELETE して管理できるのか?

結論だけ先にまとめると次の通りです。

  • People API が返す id は「Person リソースのキー」であって、安定した実体 ID ではない(多くのケースでディレクトリ オブジェクト ID と同じだが保証はない)。
  • Person を直接参照する API は公式には「一覧取得(List people)」のみで、/users/{id}/people/{person-id} などの個別取得はサポート外と考えるのが安全。
  • /people で得た id を使って実体を操作したい場合は、/directoryObjects/getByIds で型解決してから /users や /contacts などの本来のエンドポイントを叩く。
  • Outlook UI の People と People API の検索結果は、インデックスと対象ソースが異なるため一致しないのが仕様。必要に応じて /users 検索や /me/contacts 検索を併用する。
  • People(Person リソース)は読み取り専用で、作成・更新・削除は 個人連絡先(/me/contacts)やディレクトリ オブジェクト(/users, /contacts)側で行う。

Microsoft Graph People API と Person リソースの正体

まず、People API と Person リソースの役割を整理します。

People API は「関連度インデックス」であって実体データベースではない

Microsoft Graph の People API は、メール・会議・チャット・連絡先など複数のソースから、「そのユーザーにとって関連性の高い人物」をスコア付けして返すインサイト系 API です。

  • 返ってくるのは person リソース の集合
  • person は ユーザー(user)、組織の連絡先(orgContact)、Outlook 個人連絡先(contact)、最近やり取りした相手(暗黙的なコンタクト)などをまとめて表現する「ビュー」
  • 並び順は 関連度スコア(relevance) で決まり、完全な一覧ではない

Person リソースの定義を見ると、「メールや連絡先から集約した人物情報」であることが明記されています。 つまり、People API は「誰とよくやり取りしているか」を知るための API であって、「すべてのユーザーや連絡先を正確に列挙する」ための API ではありません。

personType から実体の種類を推測できる

Person には personType(最近のスキーマでは personType.class と personType.subclass の 2 つの文字列)というプロパティがあり、元になっている実体の種別をざっくり教えてくれます。

personType.classpersonType.subclass実体のイメージ典型的な Graph リソース
PersonOrganizationUser組織内ユーザーuser(/users/{id})
PersonPersonalContact自分の Outlook 個人連絡先contact(/me/contacts 等)
PersonOrganizationContact組織の連絡先(GAL)orgContact(/contacts/{id})
PersonImplicitContact最近のやり取りから推定された相手バックエンドで推定された相手(ユーザー/連絡先のいずれか)
GroupUnifiedGroup などOffice 365 グループ等group(/groups/{id})

このように、Person 自体は「混在したソースをひとまとめにしたビュー」であり、実際の CRUD はそれぞれのリソース型のエンドポイントで行う必要があります。

People 検索で返る id を /people/{id} に渡しても取得できない理由

Person の API は「一覧取得」しか公式サポートされていない

Microsoft Graph の公式ドキュメントを見ると、Person リソースのメソッドとして記載されているのは List people(一覧取得)だけです。

  • GET /me/people
  • GET /users/{id}/people

「GET /users/{user-id}/people/{person-id}」に相当する 単一 Person 取得のエンドポイントは公式には公開されていません。したがって、People API から返された id をそのまま URL に埋め込んで参照する設計は、そもそもドキュメント外の動きに依存してしまいます。

実際、Microsoft Q&A やフォーラムでも「/users/{id}/people?$search= で返ってきた id を使って /people/{id} を叩くと 404 になる」といった報告があり、Microsoft 側からも People API の ID を直接使うのではなく、ディレクトリ オブジェクト ID として解決するようにとの案内がなされています。

People の id は「インデックスキー」であり、安定したリソース ID ではない

People API のレスポンスの id は、実体のユーザーや連絡先の ID と一致することも多いのですが、仕様としてそう保証されているわけではありません。以下のようなケースを考えると、不安定さが理解しやすくなります。

  • 同じユーザーが ディレクトリ ユーザーとしても Outlook 連絡先としても存在しており、People API 側では両方を統合して 1 件の Person として扱っている場合
  • 外部ユーザーとの最近のやり取りからのみ構築された 暗黙的なコンタクト(ImplicitContact)の場合
  • サービス内部でのみ意味を持つ 一時的なキーで ID が構成されている場合

このような背景があるため、People API の id を「安定した実体 ID」と見なしてしまうと、ある日突然 404 になったり、他環境では動かないといった問題が起きます。

典型的なハマりパターン

よくあるパターンを簡易的な例で示します。

GET https://graph.microsoft.com/v1.0/users/{user-id}/people?$search=%22benjamin%22
Headers:
  Authorization: Bearer <token>
  ConsistencyLevel: eventual

レスポンス例(抜粋):

{
  "value": [
    {
      "id": "wHB******************Rg==",
      "displayName": "SomeSurname, Benjamin",
      "userPrincipalName": "[email protected]",
      "personType": {
        "class": "Person",
        "subclass": "OrganizationUser"
      }
      ...
    }
  ]
}

ここで得られた id を使って次のように叩くと、環境によっては 404 になります。

GET https://graph.microsoft.com/v1.0/users/{user-id}/people/wHB******************Rg==

理由はシンプルで、そもそもこのエンドポイントはドキュメント上のサポート対象ではないうえ、People の id が内部的なキーであったり、ユーザー以外の実体(orgContact, contact など)を表していたりするためです。

したがって、People API から返された id を直接 /people/{id} に渡して参照しようとする設計は避け、「People は検索インデックス、実体取得は別エンドポイント」という前提で組み立てる必要があります。

正攻法: /directoryObjects/getByIds で実体の種類を解決する

directoryObjects/getByIds とは

/directoryObjects/getByIds は、Azure AD(Entra ID)内のディレクトリ オブジェクト ID のリストを渡すと、実際のオブジェクト(user, group, orgContact, device など)をまとめて解決してくれる関数です。

POST https://graph.microsoft.com/v1.0/directoryObjects/getByIds
Content-Type: application/json

{
  "ids": [
    "wHB******************Rg==",
    "b3c******************AA=="
  ],
  "types": [
    "user",
    "orgContact",
    "group",
    "device",
    "servicePrincipal"
  ]
}

レスポンスには、@odata.type(#microsoft.graph.user など)と実体のプロパティが含まれるため、「これは user」「これは orgContact」など型ごとに振り分けることができます。

実装フロー(サンプル)

People 検索から実体取得までの基本フローは次の三段構えです。

  1. People で「誰か」を探す(名前やメールアドレスで候補を絞る)
  2. 見つかった人の id を /directoryObjects/getByIds に投げる(実体と型を解決する)
  3. 型ごとに本来のエンドポイントで詳細を再取得する

HTTP レベルの例:

# 1. People で候補を検索
GET https://graph.microsoft.com/v1.0/users/{user-id}/people?$search=%22benjamin%22&$top=25
Headers:
  Authorization: Bearer <token>
  ConsistencyLevel: eventual

# 2. 返ってきた value[].id を配列に集め、getByIds に投げる
POST https://graph.microsoft.com/v1.0/directoryObjects/getByIds
Content-Type: application/json

{
  "ids": [
    "wHB******************Rg==",
    "b3c******************AA=="
  ],
  "types": [
    "user","orgContact","group","device","servicePrincipal"
  ]
}

アプリ側では、@odata.type を見て以下のように振り分けます。

@odata.type実体の型再取得に使うエンドポイントの例
#microsoft.graph.userユーザーGET /v1.0/users/{id}
#microsoft.graph.orgContact組織の連絡先(GAL)GET /v1.0/contacts/{id}(orgContact)
#microsoft.graph.groupグループ(配布グループ / Microsoft 365 グループ等)GET /v1.0/groups/{id}
#microsoft.graph.deviceデバイスGET /v1.0/devices/{id}

個人連絡先(contact)の場合は、People から直接 contact ID が返るとは限らないため、mail や emailAddresses をキーにして /me/contacts を検索し直すのが現実的です。

GET https://graph.microsoft.com/v1.0/me/contacts?
    $filter=emailAddresses/any(a:a/address eq '[email protected]')

簡易な擬似コード(TypeScript 風)

async function resolvePeopleCandidates(graphClient, myUserId, query) {
  // 1. People で候補を取得
  const people = await graphClient
    .api(`/users/${myUserId}/people`)
    .search(`"${query}"`)
    .top(25)
    .header('ConsistencyLevel', 'eventual')
    .get();

  const ids = people.value.map(p => p.id);

  // 2. ディレクトリ オブジェクトとして型解決
  const directoryObjects = await graphClient
    .api('/directoryObjects/getByIds')
    .post({
      ids,
      types: ['user', 'orgContact', 'group', 'device', 'servicePrincipal']
    });

  // 3. 型ごとに分類
  const users      = directoryObjects.value.filter(o => o['@odata.type'] === '#microsoft.graph.user');
  const orgContacts = directoryObjects.value.filter(o => o['@odata.type'] === '#microsoft.graph.orgContact');
  const groups     = directoryObjects.value.filter(o => o['@odata.type'] === '#microsoft.graph.group');

  // 必要に応じて /users, /contacts, /groups で追加情報を取得
  return { people: people.value, users, orgContacts, groups };
}

ポイントは、「Person 自体を深追いしないで、速やかに directoryObject / contact / user へ橋渡しする」ことです。

Outlook の People 画面で見えるのに /people?$search でヒットしない理由

Outlook UI と People API はそもそも別のもの

Outlook の「People」ビュー(連絡先画面)は、以下のような情報源を横断的に表示しています。

  • 自分の Outlook 連絡先フォルダー(/me/contacts)
  • 組織のアドレス帳(GAL、orgContact や user)
  • 共有メールボックスやグループなど
  • キャッシュされたオートコンプリート エントリ 等

一方、People API は「そのユーザーにとって関連度が高い人」に絞ったインデックスです。

  • すべての連絡先・ユーザーが必ず含まれるわけではない
  • コミュニケーションの頻度や組織関係などのシグナルが弱い相手はインデックスから漏れることがある
  • 検索条件によっては、インデックスには存在していても上位に現れない

さらに、People API の $search パラメーターは、「サインイン中のユーザー自身の関連人物」への検索に限られると明記されています。 そのため、/users/{他人の id}/people?$search= のような呼び出しでは、期待通りに動かないケースがあります。

検索のインデックスと更新タイミングの違い

Graph の検索($search)や People の関連度インデックスは、最終的整合性(eventual consistency) モデルで管理されています。

  • 連絡先を作成・更新してすぐは、People 検索に反映されないことがある
  • メールを送り始めてから People に出てくるまで、しばらく時間がかかる
  • 表示名の表記ゆれ(全角/半角、姓・名の順序)によって検索結果が変わることもある

これに対して、Outlook の People 画面は フォルダー単位の生データを直接表示している部分も多く、「作ったら即表示」されるため、両者の挙動に差が出ます。

Outlook People vs People API の違いまとめ

観点Outlook People(UI)People API(/me/people, /users/{id}/people)
目的ユーザーにフレンドリーな「連絡先管理」画面「関連度の高い人物」を高速に検索・表示する API
対象ソースOutlook 連絡先フォルダー、GAL、共有 MBX 等ローカル連絡先+組織ディレクトリ+最近の通信相手など、関連度ベースのサブセット
結果の網羅性ほぼ網羅的(フォルダー内+ディレクトリ)「よく使う相手」に寄った部分集合
更新タイミングフォルダー更新とほぼリアルタイムインデックス更新の遅延あり(eventual consistency)
$search の挙動クライアント実装に依存サインイン中ユーザーの関連人物に対する fuzzy search(People.Read / People.Read.All が必要)

足りない分は /users および /me/contacts 検索で補う

ユーザー選択 UI(People Picker)のような実装では、People だけに頼らず、次のようなフォールバック ロジックを用意すると安定します。

  1. まず People API で検索(候補の優先順位付けに使う)
  2. ヒットが少ない場合は /users の $search / $filter で組織ユーザーを補完
  3. さらに /me/contacts 検索で個人連絡先も対象に含める

例(組織ユーザーを displayName と mail で検索):

GET https://graph.microsoft.com/v1.0/users?
    $search=%22displayName:benjamin OR mail:benjamin%22
Headers:
  Authorization: Bearer <token>
  ConsistencyLevel: eventual

例(自分の連絡先フォルダーから名前で検索):

GET https://graph.microsoft.com/v1.0/me/contacts?
    $search=%22benjamin%22
Headers:
  Authorization: Bearer <token>
  ConsistencyLevel: eventual   <!-- 一部クエリで必要 -->

People API で「誰を優先的に見せるか」を決め、/users と /me/contacts を「取りこぼし防止」に使うイメージです。

People の作成・更新・削除はできるか?

結論: People(Person リソース)は読み取り専用

Person リソースの公式ドキュメントには、一覧取得(List people)以外のメソッドは記載されておらず、People API 自体に POST / PATCH / DELETE は存在しません。

そのため、People を直接「作成」「更新」「削除」することはできません。People はあくまで、

  • 組織ディレクトリ(user, orgContact)
  • Outlook 連絡先(contact)
  • 最近のやり取り(メール・会議など)

といったデータソースから集計された結果を返す、読み取り専用のビューとして扱う必要があります。

CRUD は「実体リソース側」で行う

人物データの CRUD は、目的に応じて次のエンドポイントを使います。

やりたいこと使うエンドポイント備考
ユーザー自身の Outlook 連絡先を作成POST /me/contacts連絡先フォルダー配下に contact を追加
連絡先の更新PATCH /me/contacts/{id}名前・メール・電話番号などを編集
連絡先の削除DELETE /me/contacts/{id}ごみ箱(回復可能アイテム)への移動
組織内ユーザーを作成 / 管理POST /users, PATCH /users/{id}, DELETE /users/{id}Entra ID 上のユーザー管理。権限要件は高め。
組織の連絡先(orgContact)を参照GET /contacts, GET /contacts/{id}読み取りのみ。Graph 経由での新規作成は現状サポート外。

People に表示される内容を「編集したい」場合は、その People がどのリソース(user / orgContact / contact 等)を元にしているかを特定し、そのリソースのエンドポイントに対して操作することになります。

権限(スコープ)の整理

People やディレクトリにアクセスするには、適切な Graph 権限が必要です。

用途代表的な権限概要
自分の関連人物(/me/people)を読むPeople.Readサインイン ユーザーの People の読み取り
他ユーザーの People(/users/{id}/people)を読むPeople.Read.All組織内の任意ユーザーの People を読み取れる(管理者同意が必要)
ディレクトリ ユーザー/連絡先を読むDirectory.Read.All, OrgContact.Read.All組織のディレクトリ データ(user, orgContact など)の読み取り
自分の Outlook 連絡先の CRUDContacts.Read, Contacts.ReadWrite/me/contacts 系の操作

特に /directoryObjects/getByIds を使う場合は、Directory.Read.All が必要になる点に注意してください。

実装テンプレート: People を入り口にした人物解決フロー

HTTP ベースのひな型

API の組み合わせを、実装のひな型としてまとめておきます。

A. People で候補を検索

GET https://graph.microsoft.com/v1.0/users/{user-id}/people?
    $search=%22benjamin%22&amp;$top=25
Headers:
  Authorization: Bearer &lt;token&gt;
  ConsistencyLevel: eventual

※ /me/people を使えるなら、/users/{user-id} より /me を優先する方がシンプルです。

B. 返ってきた id を getByIds で解決

POST https://graph.microsoft.com/v1.0/directoryObjects/getByIds
Content-Type: application/json

{
  "ids": [
    "&lt;peopleで得たid1&gt;",
    "&lt;peopleで得たid2&gt;"
  ],
  "types": [
    "user",
    "orgContact",
    "group",
    "device",
    "servicePrincipal"
  ]
}

C. 種別ごとに再取得

  • user → GET /v1.0/users/{id}
  • orgContact → GET /v1.0/contacts/{id}
  • 個人連絡先候補 → GET /v1.0/me/contacts?$filter=emailAddresses/any(a:a/address eq '{mail}')

このフローを 1 つのサービス関数として実装しておけば、UI 側からは「名前やメールアドレスを渡すだけで、ユーザー・連絡先・グループを横断して検索し、型付きで返却してくれる」便利なコンポーネントとして再利用できます。

よくある落とし穴とチェックリスト

$search と ConsistencyLevel ヘッダー

Graph で $search を使うとき、特にディレクトリ オブジェクト(/users, /groups, /contacts)では、ConsistencyLevel: eventual ヘッダーが必須です。

  • [ ] $search を使うすべてのリクエストに ConsistencyLevel: eventual を付けているか
  • [ ] SDK(C#, JavaScript など)を使う場合、ヘッダー付与の方法を公式例で確認しているか

People の id をそのまま /people/{id} に投げていないか

  • [ ] People の id を /users/{id}/people/{id} や /me/people/{id} に渡して参照していないか
  • [ ] 必ず /directoryObjects/getByIds か、メールアドレスなど他のキーで実体を取り直しているか

personType / mailboxType を活用しているか

  • [ ] personType.class / personType.subclass から、おおよその種別(ユーザーか、連絡先か)を判別しているか
  • [ ] 連絡先の場合は /me/contacts 検索に自動でフォールバックしているか

Outlook UI と People API の差を前提に設計しているか

  • [ ] 「Outlook に見えているものがすべて People に出てくる」と仮定していないか
  • [ ] 必要に応じて /users / /me/contacts を併用する設計になっているか

CRUD は実体エンドポイントで行っているか

  • [ ] 連絡先の作成・更新・削除を /me/contacts 系で行っているか
  • [ ] People API に対して POST / PATCH / DELETE を試みていないか

設計指針のまとめ: 「誰を」「何で」「どこから取るか」を分離する

最後に、People API を使った人物解決ロジックの設計思想を整理しておきます。

  • People(/people)は「誰を」見せるかを決めるインデックス
    名前やメールアドレスから関連度の高い人物を高速にピックアップする用途に特化させる。
  • /directoryObjects/getByIds は「それが何か(どの型か)」を確定する型解決レイヤー
    People で得た id から、user / orgContact / group など実体の型を判別する。
  • /users, /me/contacts, /contacts などは「どこから」「どのプロパティを」取得・更新するかを担う実体アクセス レイヤー
    画面表示用のプロパティ取得、編集機能などはすべてこちらで行う。

この三層構造を意識して設計しておけば、

  • People API 側のインデックス仕様が変わっても、実体アクセス部分は最小限の修正で済む
  • Outlook や他のクライアントと同様の「人選び」体験を提供しつつ、安定した CRUD を実現できる
  • ディレクトリ ユーザーと Outlook 連絡先の混在にも柔軟に対応できる

というメリットがあります。

「People はあくまでナビゲーション用インデックス」「本物のデータは users / contacts 側にある」という整理をしておくと、Microsoft Graph を使った人物検索や People Picker の実装がぐっと扱いやすくなります。

この記事を書いた人

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

コメント

コメントする

目次