Purpose: A practical, engineer-focused reference for discovering and using Microsoft Graph endpoints that return Entra ID data.
Use this as a printable reference, a living document, or a baseline for building your own internal Graph catalog.
Microsoft Graph is Microsoft’s unified REST API that provides a single endpoint to access data and actions across Microsoft cloud services.
Instead of calling many different service-specific APIs (Azure AD, Exchange, SharePoint, Intune, Teams), Microsoft Graph exposes them through one consistent API surface.
At a high level, Microsoft Graph allows you to:
- Read and manage Entra ID (Azure AD) objects (users, groups, devices)
- Configure authentication and security policies
- Access Microsoft 365 data (mail, calendar, files)
- Manage Intune / device management
- Query audit logs, sign-ins, and reports
All Microsoft Graph calls use a single base URL:
https://graph.microsoft.comYou then choose an API version:
v1.0→ production, stablebeta→ preview, newest features (may change)
Example:
https://graph.microsoft.com/v1.0/meMicrosoft Graph is built on REST + OData principles.
Key concepts:
- Resources → users, groups, devices, policies
- Relationships → members, owners, assignments
- Actions → reset password, assign license
Example resource path:
/users/{id}/memberOf
Microsoft Graph uses OAuth 2.0 access tokens issued by Entra ID.
You must authenticate using one of these identities:
- A signed-in user (delegated permissions)
- An application / service principal (application permissions)
| Permission type | Runs as | Typical use |
|---|---|---|
| Delegated | Signed-in user | Graph Explorer, user tools |
| Application | App identity | Automation, background jobs |
Digging Into the Graph Documentation
Getting familiar with upcoming Microsoft Changes
https://developer.microsoft.com/en-us/graph/changelog/?filterBy=beta,Identity%20and%20access
Daniel Bradley MS Docs Tracker
Lokka
Graph Explorer is a browser-based tool for testing Graph requests.
- Runs as you
- Handles tokens automatically
- Best for learning and discovery
Example:
GET https://graph.microsoft.com/v1.0/meGraph Explorer
Microsoft Graph is fundamentally an HTTP REST API.
Example:
GET /v1.0/users
Authorization: Bearer {access_token}Used when:
- Building custom apps
- Calling Graph from scripts or services
Microsoft provides PowerShell modules that wrap Graph calls.
Example:
Connect-MgGraph -Scopes User.Read.All
Get-MgUserPowerShell is ideal for:
- Admin automation
- Reporting
- Bulk operations
Microsoft provides SDKs that abstract raw HTTP calls.
Supported languages include:
- C# / .NET
- JavaScript / TypeScript
- Python
- Java
SDKs handle:
- Authentication
- Paging
- Serialization
| Method | Purpose |
|---|---|
| GET | Read data |
| POST | Create objects or trigger actions |
| PATCH | Update existing objects |
| DELETE | Remove objects |
Example:
PATCH /v1.0/users/{id}GET https://graph.microsoft.com/v1.0/meReturns the signed-in user’s directory object as JSON.
In Microsoft Graph, /$value tells the service:
Return the raw value of the resource, not the JSON wrapper.
It is used when the resource represents:
- A binary stream (photo, file)
- A primitive value (string, number)
- A collection of primitive values
GET /v1.0/me/photo/$value- Returns raw binary image (JPEG/PNG)
- No JSON response
- Browser downloads or renders the image
GET /v1.0/me/photos/48x48/$valueGET /v1.0/me/mailboxSettings/timeZone/$valueReturns a plain string, not JSON metadata.
GET /v1.0/me/$value❌ Invalid because /me is a complex object, not a primitive or stream.
Typical error:
{
"error": {
"code": "BadRequest",
"message": "Cannot use $value on a complex type."
}
}| Resource type | /$value supported |
|---|---|
| Binary (photo, file) | ✅ |
| Primitive (string, int) | ✅ |
| Primitive collection | ✅ |
| Complex object (user) | ❌ |
| Navigation object (manager, memberOf) | ❌ |
| Endpoint | Result |
|---|---|
/me/photo/$value |
Raw profile image |
/me/photos/{size}/$value |
Sized photo |
/me/mailboxSettings/timeZone/$value |
Plain string |
/me/manager/$value |
❌ Not supported |
User.Read(basic signed-in user)User.Read.All(reading other users)
/$value = raw output
No JSON wrapper
Use for photos, files, and primitive values
Never use it on /me itself
GET /v1.0/users
GET /v1.0/groups
GET /v1.0/devices
GET /v1.0/directoryObjectsGET /v1.0/users/{id}
GET /v1.0/users/{id}/memberOf
GET /v1.0/users/{id}/transitiveMemberOf
GET /v1.0/users/{id}/manager
GET /v1.0/users/{id}/directReports
GET /v1.0/users/{id}/registeredDevices
GET /v1.0/users/{id}/ownedDevices
GET /v1.0/users/{id}/authentication/methodsGet the Users Object ID
https://graph.microsoft.com/v1.0/users/user1@acme.com?$select=userPrincipalName,id
Get the users Roles Assigned
https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments?$filter=principalId eq 'userobjectid'
Get the Role ID Name for the User
https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions/{roleDefinitionId}?$select=id,displayName,description
GET /v1.0/groups/{id}
GET /v1.0/groups/{id}/members
GET /v1.0/groups/{id}/transitiveMembers
GET /v1.0/groups/{id}/owners
GET v/1.0/groups/$count (Count requires a consistency level header)
GET https://graph.microsoft.com/v1.0/groups/$count?$filter=securityEnabled eq true
HEADER:
ConsistencyLevel: eventual
NOTE This is a more real world scenario to pull back the count for total security groups
Get https://graph.microsoft.com/v1.0/groups/$count?$filter=startswith(displayName,'ACME')
HEADER:
ConsistencyLevel: eventual
NOTE This is a more real world scenario to pull back the count for total groups that meet a naming convention
https://graph.microsoft.com/v1.0/groups?$select=displayName
https://graph.microsoft.com/v1.0/groups?$select=displayName&$expand=owners($select=displayName,userPrincipalName)
https://graph.microsoft.com/beta/groups?$select=displayname,grouptypes&$filter=groupTypes/any(c:c eq 'DynamicMembership')
https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,'ACME-AUTH')&$select=id,displayName
What this does
/groups → queries all groups
$filter=startswith(displayName,'ACME-AUTH') → returns only groups whose names start with ACME-AUTH
$select=id,displayName → returns only: id (object ID)/displayName
Have to switch to Beta to report on Group Owner value
https://graph.microsoft.com/beta/groups/0e4b5629-83d8-4aae-a5cb-5f0c31836116?$expand=owners
##Grab the user account object ID: Cant patch a group owner with UserPrincipalName
https://graph.microsoft.com/beta/users/jhope@acmebaseline.onmicrosoft.com?$select=id
##Now its multiple steps to patch the odata with the updated owner
POST /v1.0/groups/{GROUP_OBJECT_ID}/owners/$ref
> Replace Group Object with the ID Above
Request body update with odata and user object id:
{
"@odata.id": "https://graph.microsoft.com/v1.0/users/USER_OBJECT_ID"
}
{
"@odata.id": "https://graph.microsoft.com/v1.0/users/eaf72a07-8d1f-4324-b8dd-35d906b86d6d"
}
Check for the updated group owner information
GET https://graph.microsoft.com/beta/groups/0e4b5629-83d8-4aae-a5cb-5f0c31836116?$expand=owners
How to filter better for just the Owner Display name and id
https://graph.microsoft.com/beta/groups/0e4b5629-83d8-4aae-a5cb-5f0c31836116?$select=id,displayName&$expand=owners($select=id,displayName)
NOTE: Groups self service sign up features:
https://learn.microsoft.com/en-us/entra/identity/users/groups-self-service-management?WT.mc_id=Portal-Microsoft_AAD_IAM#group-settings
Locate the Group to convert:
https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,'AD')&$select=id,displayName
Document Group ID: (AD-AVDUsers)
GET https://graph.microsoft.com/v1.0/groups/abc11e5a-7aa8-4412-932d-f6d7c946cf7e/onPremisesSyncBehavior?$select=isCloudManaged
fce75b28-01cd-4270-b529-4989b1daf0db
Change the SOA from OnPrem to Cloud
PATCH https://graph.microsoft.com/v1.0/groups/abc11e5a-7aa8-4412-932d-f6d7c946cf7e/onPremisesSyncBehavior
REQUEST BODY:
{
"isCloudManaged": true
}
Group SOA https://learn.microsoft.com/en-us/entra/identity/hybrid/how-to-group-source-of-authority-configure
ConsistencyLevel: eventual is an HTTP request header that enables advanced queries in Microsoft Graph by allowing slightly delayed data.
ConsistencyLevel: eventualMicrosoft Graph runs across globally distributed services. To support scalability, some queries require eventual consistency.
$countstartswith()endswith()contains()- Advanced
$filterqueries
GET https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,'ACME')&$count=true
ConsistencyLevel: eventual- Data may lag briefly
- Results converge quickly
- Ideal for reporting, auditing, and inventory
None. Permissions and authorization remain unchanged.
- Required for
$countand advanced filters - Trades immediacy for scalability
- Essential for tenant-wide analysis
Typical scenarios where these Microsoft Graph query patterns are used:
- Tenant-wide reporting and inventory
- Auditing Entra ID objects (users, groups, devices)
- Conditional Access and security posture reviews
- License usage and assignment analysis
- CI/CD validation and drift detection
- Large-scale automation across tenants
Common pitfalls when working with Microsoft Graph queries:
- Forgetting to include
ConsistencyLevel: eventualwhen using$count - Assuming real-time accuracy when using eventual consistency
- Treating collection properties as single values instead of using
any() - Over-fetching data instead of using
$select - Not testing queries in both
v1.0andbetaendpoints
Counting objects in Microsoft Graph often requires:
- Scanning multiple partitions
- Aggregating results across regions
- Querying large, distributed datasets
Strong consistency cannot guarantee performance at this scale.
By using ConsistencyLevel: eventual, Microsoft Graph can efficiently compute counts while maintaining global scalability.
This tradeoff enables accurate reporting while preserving service reliability.
https://graph.microsoft.com/beta/groups?$select=displayName,groupTypes&$filter=groupTypes/any(c:c eq 'DynamicMembership')This filter means:
- Return only groups where the
groupTypescollection contains the valueDynamicMembership. - This query retrieves only dynamic groups from Microsoft Entra ID.
- The
groupTypesproperty is not a single value, it is a collection (array). - Examples of values returned by Microsoft Graph:
"groupTypes": []"groupTypes": ["Unified"]"groupTypes": ["DynamicMembership"]"groupTypes": ["Unified", "DynamicMembership"]- Because
groupTypesis a collection, you cannot filter it like this:
groupTypes eq 'DynamicMembership'- OData requires evaluating each element in the collection, which is why the
any()function is required.
groupTypes/any(c:c eq 'DynamicMembership')
| Component | Description |
|---|---|
| groupTypes | The collection property on the group object |
| any(...) | OData function that evaluates elements in a collection |
| c | Temporary variable representing each element |
| c eq 'DynamicMembership' | Condition applied to each element |
How to read this expression:
- For each value
cingroupTypes, return the group if any value equalsDynamicMembership. - If at least one element matches, the group is included in the response.
This query returns:
- Dynamic Security Groups
- Dynamic Microsoft 365 Groups
Both group types include the following value:
"groupTypes": ["DynamicMembership"]Microsoft 365 dynamic groups may also include:
"groupTypes": ["Unified", "DynamicMembership"]All Dynamic Groups
groupTypes/any(c:c eq 'DynamicMembership')Microsoft 365 (Unified) Groups Only
groupTypes/any(c:c eq 'Unified')Dynamic Microsoft 365 Groups Only
groupTypes/any(c:c eq 'Unified') and groupTypes/any(c:c eq 'DynamicMembership')Non-Dynamic Groups
not(groupTypes/any(c:c eq 'DynamicMembership'))The any() function is used throughout Microsoft Graph when filtering collection properties, including:
assignedLicenses/any(...)proxyAddresses/any(...)members/any(...)authenticationMethods/any(...)
Understanding any() allows you to:
- Reverse engineer Entra ID, Intune, and Microsoft 365 portal behavior
- Write precise and safe Microsoft Graph queries
- Avoid targeting unintended objects
- Confidently filter large datasets at scale
groupTypesis a collection, not a single valueany()checks whether at least one element matches a condition- This query returns only dynamic groups
- Mastering
any()is essential for effective Microsoft Graph filtering
GET /v1.0/applications
GET /v1.0/applications/{id}GET /v1.0/servicePrincipals
GET /v1.0/servicePrincipals/{id}
GET /v1.0/servicePrincipals/{id}/appRoleAssignments
GET /v1.0/servicePrincipals/{id}/oauth2PermissionGrantsGET /v1.0/identity/conditionalAccess/policies
GET /v1.0/identity/conditionalAccess/namedLocations
GET /v1.0/identity/conditionalAccess/authenticationStrengthshttps://graph.microsoft.com/v1.0/identity/conditionalAccess/policies
Response: Returns all Conditional Access policies with full details including:
Policy ID, display name, state (enabled/disabled/report-only)
Conditions (users, groups, applications, locations, platforms, etc.)
Grant controls (MFA, compliant device, approved app, etc.)
Session controls (sign-in frequency, persistent browser, etc.)
Authentication strength requirements
?$select=id,displayName,state
?$filter=state eq 'enabled'
(https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$select=id,displayName,state&$filter=startswith(displayName,'ACME')&$orderby=displayName)
You’re running into a Microsoft Graph beta limitation / bug, not a syntax issue on your side.
For Conditional Access policies, the /beta/identity/conditionalAccess/policies endpoint does not reliably enforce startswith() server-side. When this happens, Graph returns all policies and applies only partial or no filtering internally — which is why you’re seeing policies that do not start with ACME.
Key point (important)
There is currently no 100% reliable server-side way to return only CA policies whose displayName starts with a string.
> **Rule**: If the Entra blade is under **Protection**, the Graph path usually starts with `/identity`.
GET /v1.0/authenticationMethodsPolicy
GET /beta/authenticationMethodsPolicy
GET /beta/policies/authenticationMethodsPolicy
GET /beta/policies/authorizationPolicyPolicies/AuthorizationsPolicy
- defaultUserRolePermissions (create apps, security groups, tenants)
- allowInvitesFrom (guest invite restrictions)
- allowUserConsentForRiskyApps
- blockMsolPowerShell
- allowedToUseSSPR
- allowEmailVerifiedUsersToJoinOrganization
- guestUserRoleId
Permissions required: Policy.Read.All
Policies/AuthenticationMethodsPolicy
**This endpoint returns a single singleton policy, not a collection.**
So:
❌ $filter
❌ $select for sub-objects
❌ “enabled only”
…are not supported, because there is only one policy object per tenant.
Getting Familiar with different Authentication types
https://learn.microsoft.com/en-us/graph/api/resources/authenticationmethod?view=graph-rest-beta
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations/FIDO2
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations/temporaryAccessPass
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations/sms
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations/voice
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations/email
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations/softwareOath
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations/hardwareOath
GET /beta/authenticationMethodsPolicy/authenticationMethodConfigurations/x509Certificate
authenticationMethodsPolicy
└── authenticationMethodConfigurations
├── fido2
├── microsoftAuthenticator
├── temporaryAccessPass
├── softwareOath
├── hardwareOath
├── sms
├── voice
├── email
└── x509Certificate
🔑 Permissions required
Read: Policy.Read.AuthenticationMethod
Write: Policy.ReadWrite.AuthenticationMethod
(Global Admin or Authentication Policy Admin role required)
GET /v1.0/users/{id}/authentication/methodsGET /v1.0/auditLogs/signIns
GET /v1.0/auditLogs/directoryAuditsGET /v1.0/reports/authenticationMethodsUserRegistrationDetails
GET /v1.0/reports/credentialUserRegistrationDetails
GET /v1.0/reports/signInActivityGET /v1.0/identityGovernance/accessReviews/definitions
GET /v1.0/identityGovernance/accessReviews/instancesGET /v1.0/identityGovernance/entitlementManagement/catalogs
GET /v1.0/identityGovernance/entitlementManagement/accessPackagesGET /v1.0/organization
GET /v1.0/domains
GET /v1.0/directoryRoles
GET /v1.0/subscribedSkus
GET /beta/settings
GET /beta/directorySettingTemplatesGET https://graph.microsoft.com/beta/settings
The /settings endpoint in Microsoft Graph exposes tenant-wide directory settings in Microsoft Entra ID (Azure AD).
These settings control global identity and directory behavior and are represented as directorySetting objects.
They are not the same as users, groups, Conditional Access policies, or Intune configurations.
The endpoint returns objects of type directorySetting.
Each setting:
- Is created from a directorySettingTemplate
- Has a displayName
- Contains configurable name/value pairs
Depending on what has been explicitly configured in the tenant, you may see settings related to:
- User consent behavior
- Password and sign-in related defaults
- Guest access restrictions
- Microsoft 365 group creation permissions
- Naming policies
- Classification enforcement
- Guest ownership rules
- User consent to applications
- Admin consent workflow configuration
- Preview or legacy tenant-wide features
- Settings enabled before appearing in the portal UI
Many tenants see this response:
{
"value": []
}This is expected behavior.
Important behavior to understand:
- Directory settings only exist if they have been explicitly configured
- Default settings → ❌ not returned
- Explicitly configured settings → ✅ returned
- No object exists until a template is instantiated
To see every possible setting that could exist, query the templates:
GET https://graph.microsoft.com/beta/directorySettingTemplates
This endpoint returns:
- All supported directory setting templates
- Setting names
- Descriptions
- Allowed values
This is the authoritative way to understand:
- What settings are available
- What can be configured
- What the portal may be modifying behind the scenes
directorySettingTemplate
↓
directorySetting (instance)
↓
Configured name/value pairs
You only see /settings entries after:
- A template is instantiated
- Values are explicitly set
{
"id": "abcd-1234",
"displayName": "Group.Unified",
"values": [
{
"name": "EnableGroupCreation",
"value": "false"
},
{
"name": "AllowGuestsToBeGroupOwner",
"value": "false"
}
]
}Required Permissions
Typically required permissions include:
- Directory.Read.All — read-only
- Directory.ReadWrite.All — create or update settings
This endpoint is especially useful for:
- Auditing tenant-wide identity configuration
- Detecting hidden configuration drift
- Automating security baselines
- Reverse engineering Entra ID portal behavior
- Comparing settings across multiple tenants
This endpoint does not include:
- Conditional Access policies
- Authentication method policies
- Intune configuration profiles
- Per-user or per-group settings
Those live in other Microsoft Graph namespaces.
/beta/settingsshows only explicitly configured directory settings- Empty results are normal in many tenants
/directorySettingTemplatesshows everything that can exist- These settings control global Entra ID behavior
| Entra Portal Area | Likely Graph Root |
|---|---|
| Users / Groups | /users, /groups |
| Enterprise Apps | /servicePrincipals |
| App Registrations | /applications |
| Protection | /identity |
| Policies | /policies, /authenticationMethodsPolicy |
| Reports | /reports |
| Logs | /auditLogs |
- Identify the Entra blade (Users, Protection, Identity, Policies)
- Open Graph Explorer and start with
/beta - Inspect the response for
@odata.type - Validate properties in
$metadata - Confirm permissions in Graph Explorer
- Prefer
/v1.0for automation when available - Track beta → v1.0 promotion
When you open DevTools → Network in Entra, Microsoft 365, or Intune portals, you’ll see hundreds of requests:
- JavaScript bundles
- Fonts
- Images
- CSS
- Telemetry
- API calls
All Microsoft Graph REST calls are sent as XHR / Fetch requests. This includes:
- List queries
$countcalls- Policy reads
- Portal data loads
- Validation checks
Filtering to XHR removes UI noise and surfaces only data-fetching requests.
- Press F12
- Go to Network
- Enable Preserve log
Click Fetch/XHR to isolate Graph calls.
| Signal | Why it matters |
|---|---|
graph.microsoft.com |
Actual Graph call |
/v1.0/ or /beta/ |
API version |
$count |
Server-side counting |
$filter= |
Server-side filtering |
$select= |
Payload shaping |
ConsistencyLevel |
Advanced query support |
Portal shows:
“123 Groups”
XHR request:
GET https://graph.microsoft.com/v1.0/groups/$count?$filter=securityEnabled eq trueHeaders:
ConsistencyLevel: eventualThis is the exact API call the portal uses.
- Click the XHR request
- Copy the Request URL
- Note required headers:
ConsistencyLevel
- Ignore:
client-request-idx-ms-*headers- Telemetry values
$filter$count$orderby
Appears directly in the URL.
- No filters in the URL
- Data processed in JavaScript after retrieval
This distinction is critical for performance and scale.
Reverse-engineering Graph via XHR allows you to:
- Discover undocumented endpoints
- Identify real count queries
- Understand Graph limitations
- Reproduce portal behavior in automation
- Avoid unsupported query patterns
/{resource}/$count?$filter=startswith(displayName,'ACME')?$expand=owners($select=id,displayName)/beta/identity/...Portal UI
↓
XHR request
↓
Microsoft Graph endpoint
↓
Same call you can automate
The portal is simply a Graph client.
If the portal can display it, Graph can return it.
XHR is the fastest and most reliable way to discover the correct endpoint, headers, and query parameters.
$batch is a Microsoft Graph batching endpoint that allows the client (portal, admin center, or your code) to send multiple Graph requests in a single HTTP call.
Instead of issuing dozens of individual REST calls, the UI bundles them together and sends them as one request.
When you open something like:
- Users blade
- Groups overview
- Conditional Access
- Authentication methods
- Intune pages
…the portal often needs many different pieces of data at once:
- Object details
- Counts
- Policy state
- Related objects
- Feature flags
Rather than making multiple calls like:
GET /users
GET /users/$count
GET /directory/settings
GET /policies/conditionalAccessPoliciesThe portal sends one $batch call containing all of them.
POST https://graph.microsoft.com/beta/$batch
Content-Type: application/json{
"requests": [
{
"id": "1",
"method": "GET",
"url": "/users?$select=id,displayName"
},
{
"id": "2",
"method": "GET",
"url": "/groups/$count?$consistencyLevel=eventual"
}
]
}{
"responses": [
{
"id": "1",
"status": 200,
"body": {
"value": [ ... ]
}
},
{
"id": "2",
"status": 200,
"body": 412
}
]
}Each response maps back to its request by ID.
Most modern portal blades use $batch, so the real API endpoints are nested inside the request body.
When you expand the payload, you’ll find entries like:
"url": "/policies/conditionalAccessPolicies?$filter=..."That url value is the exact endpoint you can call directly.
In DevTools → Network → XHR:
- Click the
$batchrequest - Open the Request Payload
- Expand
requests[] - Look for:
methodurl
- Copy the
urlvalue - Prepend:
https://graph.microsoft.com/beta
✅ That’s the real API call.
You can use $batch yourself to:
- Reduce API throttling
- Improve performance
- Make atomic-style read operations
- Query multiple related resources efficiently
| Limit | Value |
|---|---|
| Requests per batch | 20 |
| Same tenant only | Yes |
| Mixed methods | GET, POST, PATCH, DELETE |
| Cross-request dependency | Limited |
Avoid $batch for:
- Large exports
- Pagination-heavy queries
- Long-running reports
- Simple single-object calls
For those scenarios, direct endpoints are cleaner and easier to manage.
This explains why:
- Sorting by XHR works
- Portal actions feel “magic”
- Graph looks imperative in PowerShell
- Lokka and MCP tools are so powerful
Everything funnels through Graph — $batch is just the transport optimization layer.
$batchbundles multiple Graph calls into one- The portal uses it everywhere
- The real endpoints are inside the request body
- Copy the
urlvalue to reproduce the call - Understanding
$batchis essential for reverse-engineering Graph
| Data Type | Permission |
|---|---|
| Users | User.Read.All |
| Groups | Group.Read.All |
| Directory | Directory.Read.All |
| Policies | Policy.Read.All |
| Auth Methods | AuthenticationMethod.Read.All |
| Logs | AuditLog.Read.All |
| Reports | Reports.Read.All |
- Most Entra innovation appears in /beta first
$metadatais always authoritative- Portal network calls reveal undocumented APIs
Audience: Entra ID engineers, identity architects, security engineers
Maintenance Tip: Review this document quarterly and diff /beta/$metadata for new resource types
This document explains what the Microsoft Graph $metadata endpoint is, how to read it, and how to translate metadata into practical OData query usage when working with Microsoft Entra (Azure AD) resources.
The $metadata endpoint exposes the entire service schema for Microsoft Graph using the OData v4 EDMX (Entity Data Model XML) format.
GET https://graph.microsoft.com/v1.0/$metadata
GET https://graph.microsoft.com/beta/$metadata
It describes:
- Entity types (users, groups, devices, etc.)
- Properties and their data types
- Navigation properties (relationships)
- Inheritance
- Collections vs singletons
- Actions and functions
Think of $metadata as the authoritative contract for Graph.
You use metadata to:
- Discover all properties (even undocumented ones)
- Understand relationships between resources
- Determine what can be selected, expanded, or filtered
- Build SDKs, schemas, or automation safely
- Explain why a query fails (unsupported filters/orderby)
The Entra Admin Center is built directly on this schema.
The metadata document is XML and follows this hierarchy:
Edmx
└── DataServices
└── Schema (Namespace="microsoft.graph")
├── EntityType
├── ComplexType
├── EnumType
├── EntityContainer
Each EntityType maps to a Graph resource.
<EntityType Name="user" BaseType="graph.directoryObject">
<Property Name="displayName" Type="Edm.String" />
<Property Name="accountEnabled" Type="Edm.Boolean" />
<Property Name="userPrincipalName" Type="Edm.String" />
<NavigationProperty Name="memberOf" Type="Collection(graph.directoryObject)" />
</EntityType>/users
/users/{id}
Every <Property> element is selectable.
<Property Name="displayName" Type="Edm.String" />GET /users?$select=id,displayNameIf it appears in metadata, it can be selected.
| EDM Type | Meaning | OData impact |
|---|---|---|
Edm.String |
Text | Filterable sometimes |
Edm.Boolean |
true/false | Often filterable |
Edm.DateTimeOffset |
Timestamp | Filterable & sortable |
Edm.Guid |
Object ID | Filterable |
Collection(...) |
Array | Limited filter support |
Navigation properties define relationships.
<NavigationProperty Name="memberOf" Type="Collection(graph.directoryObject)" />GET /users/{id}/memberOfor
GET /users/{id}?$expand=memberOf
⚠️ $expandis heavily restricted in Graph and often capped or blocked.
Many Entra objects inherit from directoryObject.
<EntityType Name="user" BaseType="graph.directoryObject" />This means users inherit:
iddeletedDateTime
Available to all directory objects.
The EntityContainer defines top-level Graph paths.
<EntityContainer Name="GraphService">
<EntitySet Name="users" EntityType="graph.user" />
</EntityContainer>Maps directly to:
/users
Metadata also defines callable operations.
<Action Name="assignLicense" IsBound="true">POST /users/{id}/assignLicenseIf it exists in metadata → it exists in Graph.
| Metadata element | OData feature |
|---|---|
| Property | $select, $filter |
| NavigationProperty | $expand, child endpoints |
| EntitySet | Root endpoint |
| EntityType | Resource schema |
| Action | POST operation |
| Function | GET operation |
Important limitation:
Metadata shows what exists — not what is indexed.
So even if a property exists:
$filtermay fail$orderbymay fail$expandmay be blocked
Example:
<Property Name="accountEnabled" Type="Edm.Boolean" />But:
$filter=accountEnabled eq true ✅
$orderby=accountEnabled ❌Indexing is a service-level decision, not metadata.
| Version | Purpose |
|---|---|
| v1.0 | Production-supported schema |
| beta | Preview, experimental, incomplete |
Always compare:
/v1.0/$metadata
/beta/$metadata
Beta often exposes:
- New authentication methods
- Passkey / FIDO2 extensions
- Governance features
- Inspect
$metadata - Identify EntityType
- Identify Properties & NavigationProperties
- Build
$selectfirst - Add
$filter - Assume client-side
$orderby - Follow
@odata.nextLink
$metadata= schema truth- OData = query language
- Graph = restricted OData implementation
- Portal = Graph + client-side logic
If you understand $metadata, you can:
- Predict Graph behavior
- Reverse engineer portal calls
- Avoid unsupported queries
- Build resilient Entra automation
This document explains how Microsoft Intune (device management) endpoints are modeled in Microsoft Graph, how to read the metadata, and how to safely use OData query options against Intune resources.
It is intentionally parallel to the Entra OData guide, but focused on:
deviceManagement/*- Intune-backed resources
- Intune-specific OData constraints
All Intune APIs live under the deviceManagement namespace in Microsoft Graph.
https://graph.microsoft.com/v1.0/deviceManagement
https://graph.microsoft.com/beta/deviceManagement
Unlike Entra directory objects, Intune data is service-backed, not directory-backed.
This distinction explains why:
$orderbyoften works in Intune$filteris more permissive$expandis still limited
GET https://graph.microsoft.com/v1.0/$metadata
GET https://graph.microsoft.com/beta/$metadata
Search within metadata for:
Namespace="microsoft.graph"
EntityType Name="managedDevice"
EntityType Name="deviceConfiguration"
All Intune entities are defined here.
| EntityType | Graph endpoint | Portal blade |
|---|---|---|
managedDevice |
/deviceManagement/managedDevices |
Devices → All devices |
deviceConfiguration |
/deviceManagement/deviceConfigurations |
Devices → Configuration profiles |
deviceCompliancePolicy |
/deviceManagement/deviceCompliancePolicies |
Devices → Compliance policies |
mobileApp |
/deviceManagement/mobileApps |
Apps → All apps |
deviceManagementScript |
/deviceManagement/deviceManagementScripts |
Devices → Scripts |
deviceHealthScript |
/deviceManagement/deviceHealthScripts |
Devices → Proactive remediations |
<EntityType Name="managedDevice">
<Property Name="id" Type="Edm.String" />
<Property Name="deviceName" Type="Edm.String" />
<Property Name="operatingSystem" Type="Edm.String" />
<Property Name="osVersion" Type="Edm.String" />
<Property Name="complianceState" Type="graph.complianceState" />
<Property Name="managementAgent" Type="graph.managementAgentType" />
<NavigationProperty Name="users" Type="Collection(graph.user)" />
</EntityType>Every <Property> is selectable.
GET /deviceManagement/managedDevices?
$select=id,deviceName,operatingSystem,complianceStateBest practice:
- Always
$select - Intune objects are large
Intune supports filtering on many properties.
GET /deviceManagement/managedDevices?
$filter=operatingSystem eq 'Windows'$filter=complianceState eq 'compliant'$filter=startswith(deviceName,'AVD')Enums (like complianceState) are defined in metadata as EnumType.
Unlike Entra directory objects, Intune supports server-side sorting.
GET /deviceManagement/managedDevices?
$orderby=deviceName asc- Property to be scalar
- No complex navigation expansion
Default page size is 100 (varies by endpoint).
"@odata.nextLink": "https://graph.microsoft.com/..."Rules:
- Always follow
nextLink - Never assume
$topreturns everything
Navigation properties define supported child routes.
<NavigationProperty Name="deviceCompliancePolicyStates"
Type="Collection(graph.deviceCompliancePolicyState)" />GET /deviceManagement/managedDevices/{id}/deviceCompliancePolicyStatesIntune strongly prefers child endpoints over $expand.
Some Intune resources support $expand, but many do not.
GET /deviceManagement/managedDevices?$expand=usersCommon outcomes:
- Partial expansion
- Throttling
- UnsupportedQuery errors
➡️ Prefer separate calls.
Actions are POST operations defined in metadata.
<Action Name="syncDevice" IsBound="true" />POST /deviceManagement/managedDevices/{id}/syncDevicePortal buttons almost always map to actions.
Assignments are modeled as child collections, not properties.
GET /deviceManagement/mobileApps/{id}/assignmentsSame pattern for:
- Configuration profiles
- Compliance policies
- Scripts
| Version | Reality |
|---|---|
| v1.0 | Stable, lagging features |
| beta | Required for most new Intune features |
Examples only in beta:
- Endpoint Privilege Management
- Advanced reporting
- Some remediation APIs
| Issue | Cause |
|---|---|
| UnsupportedQuery | $expand or invalid filter |
| Throttling | Large tenant + no $select |
| Missing data | Not following nextLink |
| Enum filter fails | Wrong enum string |
GET /deviceManagement/managedDevices?
$select=id,deviceName,operatingSystem,complianceState
&$filter=operatingSystem eq 'Windows'
&$orderby=deviceNameThen:
- Page with
nextLink - Join related data client-side
| Aspect | Entra | Intune |
|---|---|---|
| Backend | Directory | Service DB |
$orderby |
Rare | Common |
$filter |
Restricted | Flexible |
$expand |
Limited | Very limited |
| Pagination | Required | Required |
- Intune Graph is OData-friendly but opinionated
- Metadata tells you what exists, not limits
- Prefer child endpoints over
$expand - Always
$selectand page
- Annotated metadata walkthrough for
managedDevice - Assignment model deep dive
- Portal blade → Graph → metadata mapping
- PowerShell SDK vs raw REST comparison
Understanding Intune metadata + OData gives you portal-level automation without guessing.
Example Usage
- Scopes required:
Pulling all powershell scripts from Intune using the API
https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts
https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts/{deviceManagementScript-id}$select=scriptContent
Here you would copy the script content from its base64 encoding into an editor
https://www.base64decode.org/
This would give you the content of the script for review
Working Example
https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts/c7cd4ba5-ccbf-4c9a-9be5-e22cda7379b0?$select=scriptContent
In Microsoft Graph, the ? indicates the start of query options for an HTTP request. Everything after ? modifies how data is returned, not what endpoint you are calling.
GET https://graph.microsoft.com/v1.0/users?$select=id,displayName/users→ resource path (what you are querying)?→ start of query string$select→ OData query option
HTTP URLs follow a standard structure:
/path?queryKey=value&queryKey=value
In Microsoft Graph:
?starts the query section&chains multiple query options
GET /v1.0/users?$select=id,displayName,userPrincipalName&$top=10| Query Option | Purpose | Example |
|---|---|---|
$select |
Return specific properties | ?$select=id,displayName |
$filter |
Filter results | ?$filter=accountEnabled eq true |
$expand |
Include related objects | ?$expand=memberOf |
$top |
Limit results | ?$top=25 |
$orderby |
Sort results | ?$orderby=displayName |
$count |
Include count | ?$count=true |
GET /v1.0/usersReturns many default properties per user, resulting in a larger payload.
GET /v1.0/users?$select=id,displayName,userPrincipalNameReturns only the properties you explicitly request.
Benefits:
- Smaller response payloads
- Faster queries
- Clear, intentional documentation
-
$selectdoes not grant or change permissions -
Some properties:
- Require additional permissions
- Are not selectable on certain endpoints
-
$select=*is not supported in Microsoft Graph
/users → what data you are requesting
? → how the data should be returned
$select=prop → which fields are included
&$filter=... → which records are returned
### Example: Select Specific User Properties
```http
GET /v1.0/users?$select=id,displayName,userPrincipalNameDescription: Returns a list of users with only ID, display name, and user principal name.
Permissions Required:
User.Read.All
Notes:
- Use
$selectto reduce payload size - Combine with
$filterfor targeted queries
---
## 📌 Summary
- `?` starts the query string in a Graph request
- `$select` controls which properties are returned
- Using `$select` improves performance and clarity
- Always pair `$select` with the minimum required permissions
---
**Audience**: Entra ID engineers, security engineers, Graph API consumers
**Recommended Use**: Internal documentation, team wikis, API reference guides
This document provides a concise reference for the main HTTP methods used in Microsoft Graph, their purposes, and behavior.
- Purpose: Retrieve data from a resource.
- Idempotent: ✅ (safe to call multiple times without changing server state)
- Request Body: Not typically used
- Response: Returns the resource(s) in JSON format
Example:
GET /v1.0/users?$select=displayName,userPrincipalName- Purpose: Create a new resource or invoke an action.
- Idempotent: ❌ (calling multiple times may create multiple resources)
- Request Body: Required, contains data for creation or action parameters
- Response: Returns the created resource or action result
Example:
POST /v1.0/groups
Content-Type: application/json
{
"displayName": "New Team",
"mailEnabled": false,
"mailNickname": "newteam",
"securityEnabled": true
}- Purpose: Replace an existing resource entirely.
- Idempotent: ✅ (replacing the resource with the same data multiple times has no additional effect)
- Request Body: Required, must contain full resource representation
- Response: Usually returns
204 No Content
Example:
PUT /v1.0/groups/{id}
Content-Type: application/json
{
"displayName": "Updated Team Name",
"mailEnabled": false,
"mailNickname": "updatedteam",
"securityEnabled": true
}- Purpose: Update parts of an existing resource.
- Idempotent: ✅ (updating the same properties with same values multiple times has no effect)
- Request Body: Contains only the fields to update
- Response: Usually returns
204 No Content
Example:
PATCH /v1.0/groups/{id}
Content-Type: application/json
{
"displayName": "Partially Updated Name"
}- Purpose: Remove a resource.
- Idempotent: ✅ (deleting the same resource multiple times has no effect)
- Request Body: Not used
- Response: Usually returns
204 No Content
Example:
DELETE /v1.0/groups/{id}| Method | Purpose | Idempotent | Body Required | Response |
|---|---|---|---|---|
| GET | Retrieve resource(s) | ✅ | No | JSON data |
| POST | Create resource / invoke action | ❌ | Yes | Created resource / action result |
| PUT | Replace resource entirely | ✅ | Yes | 204 No Content |
| PATCH | Update part of resource | ✅ | Yes | 204 No Content |
| DELETE | Remove resource | ✅ | No | 204 No Content |
Notes:
- Idempotent methods are safe for retries.
- POST is used for creation and actions that may have side effects.
- PATCH is preferred for partial updates to reduce payload and avoid overwriting unchanged properties.
- Always check required permissions for each operation in Microsoft
Continuous Access Evaluation (CAE) is one of the most misunderstood features in Microsoft Entra ID because it behaves differently than most policy-driven features exposed through Microsoft Graph.
A common point of confusion is:
“I thought CAE was enabled by default — so why does the Graph endpoint return
ResourceNotFound?”
Both statements can be true at the same time.
This document explains what CAE actually is, how it works, and why the Graph API behaves the way it does.
Continuous Access Evaluation is a runtime enforcement capability that allows Microsoft services to re-evaluate access during an active session, rather than waiting for token expiration.
Instead of relying solely on token lifetime, CAE allows services to react immediately to security-relevant events.
Examples include:
- Password reset
- Account disabled
- User or sign-in risk change
- Conditional Access policy updates
- Role or group membership changes
When these events occur, CAE-enabled workloads can invalidate access mid-session and force reauthentication.
Yes — CAE is enabled by default, but not in the way most people expect.
- Microsoft workloads automatically use CAE where supported
- No tenant-level configuration is required
- No admin action is needed to activate baseline CAE behavior
- There is no default CAE policy object
- There is no visible tenant-wide CAE toggle
- There is no Graph resource created automatically
This distinction is critical.
- Built into Microsoft services
- Always active where supported
- Driven by service-side logic
- Requires no policy object
- Optional
- Exposed through Microsoft Graph
- Represented by a singleton policy resource
- Only exists if explicitly created or configured
CAE enforcement exists without CAE configuration.
GET https://graph.microsoft.com/beta/identity/continuousAccessEvaluationPolicyIf the policy has never been configured, Microsoft Graph returns:
{
"error": {
"code": "ResourceNotFound",
"message": "CAE settings not found."
}
}This response is expected behavior.
It does not mean:
- The endpoint is invalid
- CAE is disabled
- You lack permissions
It means:
- The CAE policy object does not exist in the tenant
The continuousAccessEvaluationPolicy is a singleton resource.
Characteristics of singleton identity policies:
- Only one instance per tenant
- Not automatically created
- Materialized only when configured
Microsoft designed CAE as:
- Implicit
- Service-driven
- Feature-gated
- Lazy-created
Graph only returns objects that exist.
If the CAE policy has never been initialized, Graph correctly returns 404.
The CAE policy does not turn CAE on or off.
It exists primarily to:
- Control migration behavior
- Manage compatibility scenarios
- Configure advanced CAE participation settings
Example response once the policy exists:
{
"id": "continuousAccessEvaluationPolicy",
"migrate": false
}This configuration fine-tunes how workloads interact with CAE — it does not enable it.
To read or manage the CAE policy (if it exists), you typically need one of the following permissions:
Policy.Read.AllPolicy.ReadWrite.ConditionalAccessDirectory.Read.All
Admin consent is required.
Permissions alone will not create the policy object.
Most Graph features follow a predictable pattern:
- Feature exists
- Policy exists
- Graph returns an object
CAE breaks this assumption.
It behaves more like:
- Token issuance logic
- Risk evaluation pipelines
- Service-side enforcement mechanisms
Graph exposes only a thin configuration layer, not the enforcement engine itself.
CAE is not validated by querying the policy endpoint.
Instead, validate CAE by observing behavior:
- Sign-in logs
- Mid-session access revocation
- Forced reauthentication after:
- Password reset
- Account disablement
- Risk elevation
- Conditional Access changes
That is where CAE truly operates.
- CAE is enabled by default at the service level
- There is no default CAE policy object
- The Graph endpoint returns 404 if the policy does not exist
- CAE enforcement is not policy-driven
- The policy endpoint represents optional configuration only
- This is expected and correct Graph behavior
This is a textbook example of how Microsoft Graph exposes APIs:
- Before GUI controls exist
- Before objects are created
- Sometimes only as optional configuration layers
Understanding this behavior:
- Prevents misdiagnosing 404 errors
- Helps reverse engineer portal behavior
- Improves confidence when exploring new Graph endpoints
CAE is a perfect example of why learning Graph teaches you how Microsoft actually builds and ships features.









