Showing posts with label OAuth2. Show all posts
Showing posts with label OAuth2. Show all posts

Saturday, May 2, 2026

Microsoft Graph API & Development Complete Guide

 

Microsoft Graph API & Development — Complete Guide

Graph Fundamentals · Authentication · OData Queries · Webhooks · Delta Query · Batch API · SDKs · Scenarios · Cheat Sheet


Table of Contents

  1. Core Concepts — Basics
  2. Authentication & Authorisation
  3. OData Queries & Key Endpoints
  4. Advanced Graph Features
  5. Graph SDKs & Power Platform Integration
  6. Scenario-Based Questions
  7. Cheat Sheet — Quick Reference

1. Core Concepts — Basics

What is Microsoft Graph and what problem does it solve?

Microsoft Graph is a unified REST API providing access to data, relationships, and intelligence across all Microsoft 365 services through a single endpoint: https://graph.microsoft.com.

The problem it solved — before Graph:

  • Each service had its own API: Exchange → EWS, SharePoint → CSOM/REST, Azure AD → AAD Graph, Teams → Teams API, OneDrive → its own endpoint
  • Developers had to learn multiple auth models, endpoint patterns, and SDKs per service
  • Cross-service scenarios (create a Teams meeting + add to calendar + notify by email) required calls to multiple separate APIs

With Microsoft Graph:

  1. Single endpoint: one URL, one SDK, one auth flow for all M365 services
  2. Rich relationships: traverse entity relationships — user → mailbox → messages → attachments, user → joined Teams → channels → messages
  3. Intelligence layer: access M365 AI insights — trending documents, people you work with, meeting insights

What Graph covers: Users, Groups, Mail, Calendar, Contacts, Teams, SharePoint, OneDrive, Planner, To-Do, OneNote, Security, Identity, Intune, Compliance, Reports, and more.


What are the two versions of Microsoft Graph API?

Version Endpoint Use
v1.0 https://graph.microsoft.com/v1.0/ Stable, production-ready, backward compatibility guaranteed. Use for all production apps.
beta https://graph.microsoft.com/beta/ Preview APIs under development. May change without notice. Prototyping only — never production.

Warning: Using beta APIs in production is a common and dangerous mistake. If a beta API changes or is removed, your production application breaks without warning.


What are delegated permissions vs application permissions?

Delegated permissions: app acts on behalf of a signed-in user. App can only do what the user can do — user's permissions are the ceiling.

Application permissions: app acts as itself with no user context. Can access all tenant data for the granted scope. Admin consent required.

Delegated (user context):
→ Requires a signed-in user
→ Token contains user claims
→ Access scoped to what the signed-in user can access
→ Consent: user or admin can consent
→ Use for: apps with interactive user sign-in
→ Example: GET /me/messages  ← signed-in user's emails only

Application (service/daemon):
→ No user — app authenticates with its own identity
→ Token contains app claims (no user)
→ Access to ALL tenant data in the granted scope
→ Consent: ADMIN ONLY
→ Use for: background services, automation, scheduled jobs
→ Example: GET /users/{userId}/messages  ← any user's emails

Critical: Application permissions are extremely powerful
Mail.Read (application) = read EVERY mailbox in the entire tenant
Always apply least privilege — request minimum permissions needed

Critical: Admin consent is required for ALL application permissions. Never grant broader permissions than the app actually needs.


What is Graph Explorer?

Graph Explorer (developer.microsoft.com/graph/graph-explorer) is a web-based tool for exploring and testing Graph API calls without writing code.

Key features:

  • Sign in with your M365 account to run real calls against your tenant
  • Library of 200+ sample queries across all workloads
  • Request builder with headers and request body
  • Full JSON response viewer with syntax highlighting
  • Permissions panel — see required permissions, consent for testing
  • Code snippets — auto-generate code in C#, JavaScript, Java, Python, PowerShell, Go

2. Authentication & Authorisation

What OAuth 2.0 flows are used for Microsoft Graph?

Auth Code Flow (delegated — web apps with user sign-in):
1. User clicks Sign In → app redirects to Entra ID login
2. User authenticates + consents to permissions
3. Entra ID returns authorisation code to redirect URI
4. App exchanges code for access token + refresh token
5. App calls Graph: Authorization: Bearer {access_token}
6. Token expires → refresh token used silently for new access token
→ Use for: web apps, mobile apps with user context

Client Credentials Flow (application — no user):
1. App authenticates to Entra ID with client ID + client secret/certificate
2. Entra ID returns access token (no user claims)
3. App calls Graph: Authorization: Bearer {access_token}
→ Use for: background services, daemons, scheduled jobs

Device Code Flow (delegated — devices without browser):
1. App requests device code from Entra ID
2. App shows user a code + URL (microsoft.com/devicelogin)
3. User opens URL on another device, enters code, authenticates
4. App polls Entra ID until auth complete
→ Use for: CLIs, IoT devices, scripting tools (PnP PowerShell uses this)

On-Behalf-Of (OBO) Flow (delegated — API calling Graph for a user):
1. User authenticates to API A
2. API A calls Graph on behalf of user using OBO
3. Graph sees original user identity in the token
→ Use for: middle-tier APIs that call Graph with user context

How do you register an app to call Microsoft Graph?

Steps:
1. Azure Portal → Entra ID → App registrations → New registration
   → Set: name, supported account types, redirect URI

2. Note credentials from Overview:
   Application (client) ID: unique app identifier in Entra ID
   Directory (tenant) ID: your Entra ID tenant identifier

3. Create credentials (Certificates & Secrets):
   → Client secret: simple but requires rotation
   → Certificate: recommended for production — more secure

4. Configure permissions (API permissions):
   → Add permission → Microsoft Graph
   → Select: Delegated permissions OR Application permissions
   → Choose required scopes

5. Grant admin consent:
   → Required for application permissions and high-privilege delegated scopes
   → "Grant admin consent for [tenant]" button

Token endpoint:
https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token

Scope formats:
https://graph.microsoft.com/.default         ← all granted permissions
https://graph.microsoft.com/User.Read        ← specific delegated scope
https://graph.microsoft.com/Mail.Send        ← specific delegated scope

What is MSAL and why should you use it?

MSAL (Microsoft Authentication Library) is Microsoft's official library for authenticating with Entra ID and acquiring tokens. Available for .NET, JavaScript/Node.js, Python, Java, Go, iOS, Android.

Why use MSAL over raw HTTP:

  1. Token caching: automatically caches and reuses tokens until expiry
  2. Token refresh: silently refreshes expired tokens — no user re-prompt
  3. Multiple flow support: handles Auth Code, Client Credentials, Device Code, OBO flows
  4. Conditional Access handling: detects claims challenges and triggers re-authentication
  5. Security built-in: implements PKCE, state parameter, nonce automatically
// MSAL Node.js — Client Credentials (app-only):
const msal = require('@azure/msal-node');

const config = {
  auth: {
    clientId: process.env.CLIENT_ID,
    authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
    clientSecret: process.env.CLIENT_SECRET
  }
};

const cca = new msal.ConfidentialClientApplication(config);
const tokenResponse = await cca.acquireTokenByClientCredential({
  scopes: ['https://graph.microsoft.com/.default']
});

const accessToken = tokenResponse.accessToken;
// Use in: Authorization: Bearer {accessToken}

3. OData Queries & Key Endpoints

What are the OData query parameters in Microsoft Graph?

$select — choose which fields to return (always use this!):
GET /me/messages?$select=subject,from,receivedDateTime,isRead
→ Returns only those fields, not the full ~40-property message object

$filter — filter results:
GET /users?$filter=department eq 'Finance'
GET /me/messages?$filter=isRead eq false and importance eq 'high'
GET /groups?$filter=startswith(displayName,'Sales')
GET /me/events?$filter=start/dateTime ge '2025-01-01T00:00:00Z'

$orderby — sort results:
GET /me/messages?$orderby=receivedDateTime desc
GET /users?$orderby=displayName asc

$top — limit results returned:
GET /users?$top=50
GET /me/messages?$top=25

$expand — include related entities inline:
GET /me/messages?$expand=attachments
GET /groups?$expand=members($select=displayName,mail)
GET /users/{id}?$expand=manager($select=displayName,mail)

$count — include total count:
GET /users?$count=true
(Requires header: ConsistencyLevel: eventual)

$search — full-text search:
GET /me/messages?$search="project proposal"
GET /users?$search="displayName:John"

Tip: Always use $select in production — the default response returns every property. This dramatically reduces payload size and improves performance at scale.


How does pagination work in Microsoft Graph?

When results exceed the page size, Graph returns an @odata.nextLink property containing the URL for the next page.

// Pagination loop (JavaScript):
let url = 'https://graph.microsoft.com/v1.0/users?$top=100&$select=displayName,mail';
let allUsers = [];

while (url) {
  const response = await graphClient.api(url).get();
  allUsers = allUsers.concat(response.value);
  url = response['@odata.nextLink'] || null;
}

console.log(`Total users: ${allUsers.length}`);

Key pagination rules:

  • Never construct pagination URLs manually — always use @odata.nextLink as-is
  • $skiptoken values are opaque — never parse or modify them
  • $skip + $top is NOT reliable for large datasets — use nextLink
  • Some endpoints have max $top values (messages: 1000 max, users: 999 max)

What are the most important Graph endpoints?

Identity / Users:
GET  /me                                    ← signed-in user profile
GET  /users                                 ← all users in tenant
GET  /users/{id}                            ← specific user
GET  /users/{id}/manager                    ← user's manager
GET  /users/{id}/directReports              ← direct reports
GET  /users/{id}/memberOf                   ← groups/roles user belongs to
GET  /me/photo/$value                       ← profile photo (binary)

Mail:
GET  /me/messages                           ← inbox messages
POST /me/sendMail                           ← send email
GET  /me/mailFolders                        ← folder list
PATCH /me/messages/{id}                     ← update (e.g., mark read)

Calendar:
GET  /me/events                             ← calendar events
POST /me/events                             ← create meeting/event
GET  /me/calendarView?startDateTime=&endDateTime=  ← events in range

Teams:
GET  /me/joinedTeams                        ← teams I'm member of
GET  /teams/{id}/channels                   ← team channels
POST /teams/{id}/channels/{id}/messages     ← send channel message
GET  /chats/{id}/messages                   ← chat messages

SharePoint / OneDrive:
GET  /me/drive                              ← my OneDrive
GET  /me/drive/root/children                ← root folder contents
GET  /sites/{siteId}/drives                 ← SharePoint document libraries
GET  /sites/{siteId}/lists                  ← SharePoint lists
GET  /sites/{siteId}/lists/{listId}/items   ← list items

Groups:
GET  /groups                                ← all M365 + security groups
POST /groups                                ← create group
GET  /groups/{id}/members                   ← group members
POST /groups/{id}/members/$ref              ← add member to group

Reports (admin only — Reports.Read.All):
GET  /reports/getEmailActivityCounts(period='D30')
GET  /reports/getTeamsUserActivityCounts(period='D30')
GET  /reports/getSharePointSiteUsageDetail(period='D30')

What is the difference between /me and /users/{id}?

/me — delegated tokens only:
→ Shortcut for the signed-in user
→ Only works when a user is signed in
→ Returns data for the authenticated user

GET /me/messages       ← my emails
GET /me/events         ← my calendar
GET /me/drive          ← my OneDrive

/users/{id} — delegated OR application tokens:
→ Explicitly specifies a user by ID or UPN
→ Required for daemon apps and admin scenarios

GET /users/alice@contoso.com/messages  ← Alice's emails
GET /users/{objectId}/events           ← specific user's calendar

Application permission scenarios always use /users/{id}:
→ Background compliance scanning of all mailboxes
→ Admin reporting tool reading all user profiles
→ Provisioning service creating calendar events for onboarded users

4. Advanced Graph Features

What are Microsoft Graph change notifications (webhooks)?

Change notifications allow your application to receive real-time notifications when M365 resources change — instead of polling repeatedly.

1. Register a subscription:
POST https://graph.microsoft.com/v1.0/subscriptions
{
  "changeType": "created,updated,deleted",
  "notificationUrl": "https://yourapp.com/webhook",
  "resource": "/me/messages",
  "expirationDateTime": "2025-04-01T00:00:00Z",
  "clientState": "secretClientState"
}

2. Validate endpoint (Graph sends validation request):
POST https://yourapp.com/webhook?validationToken=abc123
→ Your endpoint MUST echo back validationToken within 10 seconds

3. Receive change notifications:
{
  "value": [{
    "subscriptionId": "...",
    "changeType": "created",
    "resource": "users/abc/messages/xyz",
    "clientState": "secretClientState",
    "resourceData": { "@odata.type": "#microsoft.graph.message", "id": "xyz" }
  }]
}

4. Fetch the changed resource:
GET /me/messages/xyz

Key requirements:
→ Subscriptions expire (max 4,230 minutes for most resources)
→ Renew before expiry: PATCH /subscriptions/{id}
→ Always validate clientState on incoming notifications (security)
→ Respond 202 Accepted immediately, then process async
→ Rich notifications: include resource data in the payload

Tip: Always respond 202 Accepted immediately — then process the notification asynchronously. Slow responses cause Graph to mark your endpoint unhealthy and stop sending notifications.


What is Microsoft Graph Delta Query?

Delta Query returns only the changes (new, updated, deleted items) since the last call — without re-fetching the full dataset.

Initial full sync:
GET /users/delta?$select=displayName,mail,department

Response includes:
{
  "@odata.deltaLink": "https://graph.microsoft.com/v1.0/users/delta?$deltatoken=abc123",
  "value": [ ...all users... ]
}
→ Store the deltaLink token

Incremental sync (call every 15 minutes):
GET https://graph.microsoft.com/v1.0/users/delta?$deltatoken=abc123
→ Returns ONLY users added, updated, or deleted since last call
→ Provides new deltaLink token for next call

Deleted items appear as:
{ "id": "xyz", "@removed": { "reason": "deleted" } }

Resources supporting delta:
users, groups, messages, events, contacts,
directoryObjects, teams, channels, driveItems

Use cases:
→ User provisioning: sync new/changed/deleted users to HR system
→ Calendar sync: get only new/changed meetings
→ Teams membership: track who joined/left teams
→ Mail processing: process only new emails since last run

Tip: Delta Query is the correct answer to any "how do you efficiently sync M365 data to an external system" question. Never poll the full resource list — use delta for incremental changes only.


What is the Microsoft Graph Batch API?

The Batch API combines up to 20 individual Graph requests into a single HTTP request — reducing round trips.

POST https://graph.microsoft.com/v1.0/$batch
{
  "requests": [
    {
      "id": "1",
      "method": "GET",
      "url": "/me/profile"
    },
    {
      "id": "2",
      "method": "GET",
      "url": "/me/messages?$top=5&$select=subject,from"
    },
    {
      "id": "3",
      "method": "GET",
      "url": "/me/joinedTeams"
    }
  ]
}
Response:
{
  "responses": [
    { "id": "1", "status": 200, "body": { ...profile... } },
    { "id": "2", "status": 200, "body": { ...messages... } },
    { "id": "3", "status": 200, "body": { ...teams... } }
  ]
}

Batch API rules:

  • Up to 20 requests per batch
  • Each request is independent — one failure doesn't affect others
  • Responses may be out of order — matched by id field
  • Dependency chaining via "dependsOn": ["1"] for sequential operations
  • All requests use the same auth token

How do you handle throttling in Microsoft Graph?

Throttling occurs when too many requests are made in a short period. Graph returns 429 Too Many Requests with a Retry-After header.

// Exponential backoff with Retry-After:
async function callGraphWithRetry(url, headers, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, { headers });

    if (response.status !== 429) {
      return await response.json();
    }

    const retryAfter = parseInt(response.headers.get('Retry-After') || '30');
    const backoff = Math.pow(2, attempt) * 1000;
    const delay = Math.max(retryAfter * 1000, backoff);

    console.log(`Throttled. Retrying after ${delay}ms (attempt ${attempt + 1})`);
    await new Promise(resolve => setTimeout(resolve, delay));
  }
  throw new Error('Max retries exceeded — request consistently throttled');
}

Prevention strategies:

  • Always use $select to reduce response payload
  • Use Delta Query instead of full re-fetch
  • Use Batch API to consolidate requests
  • Implement request queuing with rate limiting
  • Distribute large jobs across time periods (avoid bursts)

5. Graph SDKs & Power Platform Integration

What are the Microsoft Graph SDKs?

SDK Language Package
Graph SDK for .NET C#, F#, VB.NET Microsoft.Graph + Azure.Identity
Graph JavaScript SDK Node.js, browser @microsoft/microsoft-graph-client + @azure/msal-node
Graph SDK for Python Python msgraph-sdk + azure-identity
Graph SDK for Java Java, Android microsoft-graph + azure-identity
Graph PowerShell SDK PowerShell Microsoft.Graph module
// C# Graph SDK — app-only (client credentials):
using Azure.Identity;
using Microsoft.Graph;

var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(credential);

var users = await graphClient.Users.GetAsync(config => {
  config.QueryParameters.Select = new[] { "displayName", "mail", "department" };
  config.QueryParameters.Filter = "department eq 'Finance'";
  config.QueryParameters.Top = 50;
});

foreach (var user in users.Value) {
  Console.WriteLine($"{user.DisplayName} - {user.Mail}");
}
// JavaScript SDK — delegated:
const { Client } = require('@microsoft/microsoft-graph-client');
const graphClient = Client.initWithMiddleware({ authProvider });

const messages = await graphClient
  .api('/me/messages')
  .select('subject,from,receivedDateTime,isRead')
  .filter('isRead eq false')
  .top(25)
  .get();

What is the Microsoft Graph PowerShell SDK?

The Graph PowerShell SDK replaces the deprecated Azure AD PowerShell (AzureAD) and MSOnline (MSOL) modules.

# Install and connect:
Install-Module Microsoft.Graph -Scope CurrentUser
Connect-MgGraph -Scopes "User.Read.All","Group.ReadWrite.All"

# Users:
Get-MgUser -UserId "alice@contoso.com"
Get-MgUser -Filter "department eq 'Finance'" -Select DisplayName,Mail
New-MgUser -DisplayName "Bob Smith" -MailNickname "bsmith" -AccountEnabled $true ...
Update-MgUser -UserId $userId -Department "Engineering"

# Groups:
Get-MgGroup -Filter "startswith(displayName,'Sales')"
Add-MgGroupMember -GroupId $groupId -DirectoryObjectId $userId
Remove-MgGroupMemberByRef -GroupId $groupId -DirectoryObjectId $userId

# Mail:
Get-MgUserMessage -UserId $userId -Filter "isRead eq false" -Top 25

# Migration from deprecated modules:
# AzureAD module → Microsoft.Graph PowerShell SDK
Get-AzureADUser         → Get-MgUser
New-AzureADUser         → New-MgUser
Get-AzureADGroup        → Get-MgGroup
Add-AzureADGroupMember  → Add-MgGroupMember
# MSOnline module → Microsoft.Graph PowerShell SDK
Get-MsolUser            → Get-MgUser

Warning: Azure AD PowerShell (AzureAD module) and MSOnline (MSOL) are fully deprecated as of March 2025. All scripts must be migrated to Microsoft.Graph PowerShell SDK — a critical migration point for every M365 admin.


How do you use Microsoft Graph in Power Automate?

Option 1: HTTP with Azure AD connector (recommended):
Action: HTTP
  Method: GET
  URI: https://graph.microsoft.com/v1.0/users?$select=displayName,mail
       &$filter=department eq 'Finance'
  Authentication: Active Directory OAuth
    Tenant: [tenant ID]
    Audience: https://graph.microsoft.com
    Client ID: [app registration client ID]
    Secret: [from Key Vault reference]
→ Follow with Parse JSON to work with response

Option 2: Office 365 Users connector (standard — no premium):
→ Get user profile, manager, direct reports, search users
→ No premium licence required
→ Limited to user-related operations only

Office 365 Users in Power Apps:
Office365Users.UserProfile("alice@contoso.com").DisplayName
Office365Users.ManagerV2({userId: User().Email}).DisplayName
Office365Users.DirectReports(User().Email)

6. Scenario-Based Questions

Scenario: Build a user provisioning system syncing Azure AD users to an external HR database.

  1. App registration: application permission User.Read.All. Certificate authentication (not client secret) for production. Admin consent granted.

  2. Initial full sync:

    GET /users/delta?$select=displayName,mail,department,jobTitle,accountEnabled
    

    Process all users. Store the final @odata.deltaLink token in your database.

  3. Incremental sync (scheduled every 15 minutes):

    GET /users/delta?$deltatoken={stored_token}
    

    Process only changed/added/deleted users. Update stored token.

  4. Deletion handling: when @removed appears → mark user inactive in HR system.

  5. Pagination: follow @odata.nextLink until all results retrieved before storing the final deltaLink.

  6. Error handling: exponential backoff for 429 responses. Dead-letter queue for failed sync items with retry logic.


Scenario: Build a Teams bot that notifies a channel when a new SharePoint list item is created.

  1. App registration: permissions Sites.Read.All + ChannelMessage.Send

  2. Subscribe to SharePoint list changes:

    POST /subscriptions
    {
      "changeType": "created",
      "notificationUrl": "https://yourbot.azurewebsites.net/webhook",
      "resource": "/sites/{siteId}/lists/{listId}/items",
      "expirationDateTime": "2025-04-30T00:00:00Z",
      "clientState": "mySecretState"
    }
    
  3. Webhook endpoint: validate clientState. Respond 202 Accepted immediately. Process async.

  4. Fetch the new item:

    GET /sites/{siteId}/lists/{listId}/items/{itemId}?$expand=fields
    
  5. Post to Teams channel:

    POST /teams/{teamId}/channels/{channelId}/messages
    {
      "body": {
        "contentType": "html",
        "content": "<b>New item:</b> {item.fields.Title}"
      }
    }
    
  6. Subscription renewal: Azure Function timer — renew subscriptions before expiry using PATCH /subscriptions/{id}.


Scenario: Send an email with attachments using Microsoft Graph.

Permission: Mail.Send (delegated = user's own email, application = any user's email)

POST /users/{userId}/sendMail
{
  "message": {
    "subject": "Q1 Project Update",
    "body": {
      "contentType": "HTML",
      "content": "<h1>Q1 Update</h1><p>Please see attached.</p>"
    },
    "toRecipients": [
      { "emailAddress": { "address": "alice@contoso.com", "name": "Alice" } }
    ],
    "ccRecipients": [
      { "emailAddress": { "address": "manager@contoso.com" } }
    ],
    "attachments": [
      {
        "@odata.type": "#microsoft.graph.fileAttachment",
        "name": "Q1_Report.pdf",
        "contentType": "application/pdf",
        "contentBytes": "base64EncodedFileContent=="
      }
    ],
    "importance": "high"
  },
  "saveToSentItems": true
}

For large attachments (> 3MB):
POST /me/messages/{id}/attachments/createUploadSession
→ Upload in chunks up to 4MB each
→ Better reliability for large files

Scenario: Build an M365 usage reporting dashboard.

  1. Permission: Reports.Read.All as application permission (admin consent required)

  2. Available report endpoints:

    GET /reports/getEmailActivityCounts(period='D30')
    GET /reports/getTeamsUserActivityCounts(period='D30')
    GET /reports/getSharePointSiteUsageDetail(period='D30')
    GET /reports/getOneDriveActivityUserDetail(period='D30')
    GET /reports/getM365AppUserDetail(period='D30')
    GET /reports/getMailboxUsageDetail(period='D30')
    
  3. Response format: CSV by default. Pass Accept: application/json header for JSON.

  4. Privacy note: user display names are anonymised by default. Enable "Display concealed user, group, and site names in reports" in M365 Admin → Settings → Reports to show real names.

  5. Power BI integration: use Power BI's built-in M365 Usage Analytics template or import JSON reports for custom dashboards.


7. Cheat Sheet — Quick Reference

Permission Quick Reference

Permission type:  Delegated          Application
User context:     Required           None (no user)
Token contains:   User + app claims  App claims only
Consent:          User or admin      ADMIN ONLY
Use for:          Interactive apps   Background services
Max scope:        What user can do   All tenant data

Common delegated scopes:
User.Read          → read signed-in user's profile
Mail.Read          → read signed-in user's email
Mail.Send          → send mail as signed-in user
Calendars.ReadWrite → manage signed-in user's calendar
Sites.Read.All     → read all SharePoint sites
Group.Read.All     → read all groups

Common application scopes:
User.Read.All      → read all users in tenant
Mail.Read          → read ALL mailboxes in tenant
Calendars.Read     → read ALL calendars in tenant
Sites.Read.All     → read all SharePoint content
Reports.Read.All   → read M365 usage reports

OData Quick Reference

Filter operators:
eq, ne, gt, ge, lt, le    → equals, not-equals, comparison
and, or, not              → logical operators
startswith(prop, 'value') → string starts with
endswith(prop, 'value')   → string ends with
contains(prop, 'value')   → string contains

Common filter patterns:
$filter=department eq 'Finance'
$filter=isRead eq false
$filter=startswith(displayName,'J')
$filter=createdDateTime ge '2025-01-01T00:00:00Z'
$filter=assignedLicenses/any(x:x/skuId eq {skuGuid})

Combine parameters:
/users?$select=displayName,mail&$filter=department eq 'IT'&$orderby=displayName&$top=50

Webhook Subscription Limits

Resource                           Max expiration
Messages (mail)                    4,230 minutes (~3 days)
Calendar events                    4,230 minutes
Users / Groups (directory)         41,760 minutes (~29 days)
Teams channels / chats             60 minutes
SharePoint list items              4,230 minutes
driveItem (OneDrive/SPO)           4,230 minutes

Renewal: PATCH /subscriptions/{id}
  { "expirationDateTime": "new-expiry-datetime" }

Best practice: renew at 75% of subscription lifetime
Schedule a timer function to check and renew all active subscriptions

Delta Query Support

Supports delta:
/users/delta               → user provisioning sync
/groups/delta              → group membership sync
/me/messages/delta         → incremental mail processing
/me/events/delta           → calendar sync
/me/contacts/delta         → contacts sync
/directoryObjects/delta    → all directory changes
/teams/delta               → Teams changes
/sites/{id}/lists/{id}/items/delta  → SharePoint list sync

deltaLink: use to get changes since last call
nextLink:  use to page through large initial sync
@removed:  indicates deleted item { "reason": "deleted" | "changed" }

Graph SDK Comparison

.NET SDK:
Install-Package Microsoft.Graph Azure.Identity
var client = new GraphServiceClient(credential);
var users = await client.Users.GetAsync(...);

JavaScript SDK:
npm install @microsoft/microsoft-graph-client @azure/msal-node
const client = Client.initWithMiddleware({ authProvider });
const result = await client.api('/users').select('displayName').get();

Python SDK:
pip install msgraph-sdk azure-identity
client = GraphServiceClient(credentials=credential, scopes=scopes)
users = await client.users.get()

PowerShell SDK:
Install-Module Microsoft.Graph
Connect-MgGraph -Scopes "User.Read.All"
Get-MgUser -Filter "department eq 'Finance'"

Top 10 Tips

  1. Single endpoint, one auth flow — Graph's key value proposition over pre-Graph era. https://graph.microsoft.com covers all M365 services. Demonstrate you understand this unification.

  2. v1.0 for production, beta never in production — beta APIs can change or be removed without notice. Always a key rule to state in any Graph API design discussion.

  3. Application permissions require admin consent — and give access to ALL tenant data in the granted scope. Always request minimum permissions (least privilege). This shows security awareness.

  4. Always use $select — the default response returns every property. $select dramatically reduces payload size and improves performance at scale. Mention this in every query discussion.

  5. Delta Query for sync, not full re-fetch — the correct answer to any "how do you sync M365 data to an external system" question. Initial full sync + incremental delta = efficient synchronisation.

  6. Change notifications over polling — webhooks push changes to your app in real time. Polling the API every minute is wasteful and gets throttled. Always recommend change notifications for event-driven scenarios.

  7. 202 Accepted immediately, then process async — webhook endpoints must respond within a few seconds. Never do synchronous processing in the webhook handler — queue the work and process it asynchronously.

  8. Retry-After header on 429 — always honour the Retry-After value from throttled responses. Implement exponential backoff. Ignoring throttle responses is a common production bug.

  9. Azure AD PowerShell is deprecated (March 2025) — AzureAD and MSOnline modules are dead. All admin scripts must use Microsoft.Graph PowerShell SDK. Knowing this migration is critical for admin.

  10. Graph Explorer is your best friend — being fluent in Graph Explorer demonstrates practical experience immediately. Know how to use the sample queries, permissions panel, and code snippet generation.



Wednesday, April 15, 2026

Power Platform + Postman: The Complete Developer Guide

Power Platform Developer Series

Power Platform + Postman:
The Complete Developer Guide

✍ Sreekanth Udayagiri  |  📅 April 2026  |  ⏱ 15 min read

Postman is one of the most popular API development and testing tools used by developers worldwide. When working with Microsoft Power Platform, Postman plays multiple roles: as an API client, a testing harness, a debugging tool, a connector generator, and even as an integration target.

In this post, I'll walk you through all 6 ways Postman is used with Power Platform — with sample code and step-by-step instructions for each, based entirely on official Microsoft documentation.

💡
Based on Official Microsoft DocsMicrosoft officially supports Postman for Power Platform development. Their documentation lists Postman alongside Insomnia, Bruno, and curl as recommended API client tools for Dataverse Web API. All steps in this guide are verified against Microsoft Learn.
01

Dataverse Web API

Connect, authenticate, and run CRUD operations on Dataverse tables

02

Power Automate HTTP Trigger

Test and trigger Power Automate flows via HTTP endpoints

03

Custom Connectors

Import Postman Collections to auto-generate custom connectors

04

Custom APIs in Dataverse

Test plugin-backed custom API messages via Postman

05

Power Pages Web API

Test portal table permissions and data operations

06

Native Power Automate Integration

Backup collections, send monitor results to Power Automate


1

Postman with Dataverse Web API — Setup + CRUD

Connect Postman to your Power Apps / Dataverse environment and run Create, Read, Update, Delete operations

Microsoft Dataverse exposes a RESTful Web API based on OData v4. Postman lets you authenticate to this API and run queries — without writing any code. This is useful for debugging, data exploration, and verifying Power Platform logic.

Step 1 — Get Your Dataverse Web API URL

1
Go to make.powerapps.com— Select your environment from the top-right environment picker.
2
Click the Settings (⚙) icon→ select Developer resources.
3
Copy the Web API endpoint.It looks like:
https://yourorg.api.crm.dynamics.com/api/data/v9.2
Remove /api/data/v9.2 — keep only the part ending in .dynamics.com

Step 2 — Create Postman Environment (6 Variables)

In Postman, go to Environments → + New. Add these 6 variables:

VariableInitial ValueNotes
urlhttps://yourorg.api.crm.dynamics.comYour Dataverse org URL — no trailing slash
clientid51f81489-12ee-4a9e-aaae-a2591f45987dMicrosoft's pre-registered app ID — works for all Dataverse environments. No Azure App Registration needed.
version9.2Latest stable Web API version
webapiurl{{url}}/api/data/v{{version}}/Composite variable — base URL for all requests
redirecturlhttps://localhostOAuth redirect URI
authurlhttps://login.microsoftonline.com/common/oauth2/authorize?resource={{url}}Azure AD authorization endpoint
⚠️
ImportantThe clientid value 51f81489-12ee-4a9e-aaae-a2591f45987d is a Microsoft-provided application ID pre-approved for all Dataverse environments. This is documented on Microsoft Learn. You do NOT need to register your own Azure AD app to get started.

Step 3 — Authenticate with OAuth 2.0

Create a new HTTP request. In the Authorization tab, set:

Postman Auth Tab ConfigurationOAuth 2.0
// Authorization Tab → Type: OAuth 2.0
// Add auth to: Request Headers
// Configure New Token section:

Token Name    : DataverseToken
Grant Type    : Implicit
Auth URL      : {{authurl}}
Client ID     : {{clientid}}
Redirect URL  : {{redirecturl}}
Client Auth   : Send client credentials in body

// Click "Get New Access Token" → Sign in with your M365 account
// Click "Use Token" → Token is added to Authorization header automatically

Step 4 — Verify Connection with WhoAmI

WhoAmI — Test ConnectionGET
URL: {{webapiurl}}WhoAmI

// Required Headers (add via Header Manager)
Accept          : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0

// Expected Response — 200 OK
{
  "@odata.context": "https://yourorg.api.crm.dynamics.com/api/data/v9.2/$metadata#...",
  "BusinessUnitId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "UserId":         "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "OrganizationId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

CRUD Operations — Sample Requests

GET Retrieve Accounts (top 3, with filter)

Retrieve RecordsGET
// Get top 3 accounts — select specific fields
URL: {{webapiurl}}accounts?$select=name,telephone1,emailaddress1&$top=3

// Get single record by GUID
URL: {{webapiurl}}accounts(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)?$select=name,revenue

// Filter — accounts in a specific city
URL: {{webapiurl}}accounts?$filter=address1_city eq 'Bangalore'&$select=name,address1_city

// Expand — contacts linked to an account
URL: {{webapiurl}}accounts?$select=name&$expand=contact_customer_accounts($select=fullname,emailaddress1)

POST Create a new Contact

Create RecordPOST
URL: {{webapiurl}}contacts

// Headers
Content-Type    : application/json
Accept          : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0

// Body — raw JSON
{
  "firstname":      "Sreekanth",
  "lastname":       "Test",
  "emailaddress1":  "test@example.com",
  "telephone1":     "+91-9999999999",
  "jobtitle":       "M365 Administrator"
}

// Response: 204 No Content
// New record GUID is in the OData-EntityId response header

PATCH Update an existing record

Update RecordPATCH
URL: {{webapiurl}}contacts(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)

// Headers — include If-Match: * to avoid 412 Precondition Failed
Content-Type    : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0
If-Match        : *

// Body — only include fields you want to update
{
  "jobtitle":   "Senior M365 Administrator",
  "telephone1": "+91-8888888888"
}

// Response: 204 No Content = success

DELETE Delete a record

Delete RecordDELETE
URL: {{webapiurl}}contacts(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)

// No request body needed
// Response: 204 No Content = successfully deleted

2

Testing Power Automate HTTP Request Triggers

Use Postman to manually fire Power Automate flows via HTTP endpoints

Power Automate's "When an HTTP request is received" trigger generates a unique URL endpoint. Postman is the ideal tool to test these flows — send JSON payloads, inspect responses, and validate flow logic without building a frontend first.

Scenario A — Anonymous Trigger (Anyone with URL)

The simplest case — no authentication required. Suitable for dev/test only; not recommended for production.

Power Automate Anonymous HTTP TriggerPOST
// Copy the HTTP POST URL from the trigger card in Power Automate
URL: https://prod-xx.westus.logic.azure.com:443/workflows/<workflow-id>/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=<signature>

// Headers
Content-Type: application/json
Accept      : application/json

// Body — must match the JSON Schema defined in the trigger
{
  "employeeId":   "EMP001",
  "employeeName": "John Doe",
  "department":   "IT",
  "action":       "offboard"
}

// Response: 202 Accepted — flow triggered successfully

JSON Schema to Define in Power Automate Trigger

JSON Schema — Paste into Power Automate triggerJSON
{
  "type": "object",
  "properties": {
    "employeeId":   { "type": "string" },
    "employeeName": { "type": "string" },
    "department":   { "type": "string" },
    "action":       { "type": "string" }
  }
}

Scenario B — Tenant-Restricted Trigger (Entra ID OAuth Required)

When a flow is restricted to "Any user in my tenant", Postman must pass a bearer token. Two steps: get the token first, then call the flow.

Step 1 — Get Bearer Token from Entra IDPOST
URL: https://login.microsoftonline.com/<YOUR_TENANT_ID>/oauth2/v2.0/token

// Body: x-www-form-urlencoded (NOT JSON)
grant_type   : client_credentials
client_id    : <YOUR_ENTRA_APP_CLIENT_ID>
client_secret: <YOUR_ENTRA_APP_CLIENT_SECRET>
scope        : https://service.flow.microsoft.com//.default

// ⚠️ CRITICAL: Use double slash before .default in scope
// Single slash → "MisMatchingOAuthClaims" error

// Response
{
  "access_token": "eyJ0eXAiOiJKV1Qi...",
  "token_type":   "Bearer",
  "expires_in":   3599
}
Step 2 — Call the Tenant-Restricted FlowPOST
URL: https://<environmentID>.environment.api.powerplatform.com/powerautomate/automations/direct/workflows/<flow-id>/triggers/manual/paths/invoke?api-version=1&sp=/triggers/manual/run&sv=1.0&sig=<sig>

// Headers
Authorization: Bearer <access_token_from_step_1>
Content-Type : application/json
Accept       : application/json

// Body
{
  "message":     "Hello from Postman",
  "requestedBy": "Sreekanth"
}
💡
Pro Tip — Auto Token Refresh with Pre-request ScriptAdd a Pre-request Script in Postman to auto-fetch a fresh token before every request. This saves time when testing repeatedly.
Pre-request Script — Auto Token RefreshJavaScript
// Paste this in: Pre-request Script tab of your collection/request
const tokenUrl = `https://login.microsoftonline.com/${pm.environment.get('tenantId')}/oauth2/v2.0/token`;

pm.sendRequest({
  url   : tokenUrl,
  method: 'POST',
  header: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body  : {
    mode: 'urlencoded',
    urlencoded: [
      { key: 'grant_type',    value: 'client_credentials' },
      { key: 'client_id',     value: pm.environment.get('clientId') },
      { key: 'client_secret', value: pm.environment.get('clientSecret') },
      { key: 'scope',         value: 'https://service.flow.microsoft.com//.default' }
    ]
  }
}, (err, res) => {
  if (!err) {
    pm.environment.set('bearerToken', res.json().access_token);
  }
});

3

Creating Custom Connectors from a Postman Collection

Import a Postman Collection into Power Automate to auto-generate a Custom Connector — no Swagger file needed

If you have a third-party REST API, you can build a Postman Collection for it, export it, and Power Automate will automatically generate a Swagger definition and create a Custom Connector. This saves hours of manual API definition work.

Step 1 — Build and Export Your Postman Collection

Sample Collection JSON (V1 format)JSON
{
  "info": {
    "name":   "ServiceNow KB API",
    "schema": "https://schema.getpostman.com/json/collection/v1/collection.json"
  },
  "item": [
    {
      "name": "Get KB Articles",
      "request": {
        "method": "GET",
        "header": [
          { "key": "Accept",        "value": "application/json" },
          { "key": "Authorization", "value": "Bearer {{token}}" }
        ],
        "url": "{{baseUrl}}/api/now/table/kb_knowledge?sysparm_limit=10"
      }
    },
    {
      "name": "Create KB Article",
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json" }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\"short_description\":\"Test Article\",\"text\":\"Content here\"}"
        },
        "url": "{{baseUrl}}/api/now/table/kb_knowledge"
      }
    }
  ]
}
1
In Postman— right-click your collection →Export
2
Select"Collection v1"format → click Export → save the JSON file locally.

Step 2 — Import into Power Automate

1
Go tomake.powerautomate.com → Data → Custom Connectors → + New custom connector → Import a Postman collection
2
Upload your exported V1 JSON file. Power Automate parses it and generates Swagger automatically.
3
Review auto-generated Actions. Configure theSecurity tab(OAuth 2.0, API Key, or Basic Auth).
4
ClickCreate Connector. Now available in Power Automate flows and Power Apps.
🎯
Best Use CaseWhen you have a third-party API (ServiceNow, Salesforce, any internal REST API) and want to call it from Power Automate or Power Apps without writing code — and you don't have a Swagger/OpenAPI spec ready.

4

Testing Custom APIs in Dataverse

Invoke and test your custom Dataverse API messages — plugin-backed actions, functions, and business events

Dataverse Custom APIs let developers define their own messages (actions or functions) — typically backed by C# plugins. Postman is the recommended tool for testing these during development. All three types of custom API calls are shown below.

Testing a Custom Unbound Action

Custom Unbound ActionPOST
// Unbound = not tied to a specific table record
URL: {{webapiurl}}myapi_CustomUnboundAPI
// Format: [publisher_prefix]_[APIName]

// Headers
Content-Type    : application/json
Accept          : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0

// Body — your custom API's input parameters
{
  "InputParam1": "SampleValue",
  "InputParam2": 42,
  "FlagParam":   true
}

Testing a Custom Bound Action (tied to a record)

Custom Bound ActionPOST
// Bound to a specific table and record GUID
URL: {{webapiurl}}accounts(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)/Microsoft.Dynamics.CRM.myapi_BoundAction

// Body
{
  "ActionType": "Approve",
  "Comment":    "Approved via Postman test"
}

Testing a Custom Function (read-only, uses GET)

Custom FunctionGET
// Functions use GET — parameters go in the URL, not the body
URL: {{webapiurl}}myapi_GetEmployeeDetails(EmployeeId='EMP001',Department='IT')

// Expected response — your plugin's output parameters
{
  "@odata.context": "...",
  "EmployeeName":   "Sreekanth",
  "Status":         "Active",
  "RolesAssigned":  ["M365 Admin", "Power Platform Maker"]
}
📌
Create Custom APIs via Postman (Deep Insert)Microsoft supports creating Custom API records directly via Postman using deep insert — a single POST to {{webapiurl}}customapis. Useful for rapid iteration: create, test, delete, recreate — all without going to the maker portal.

5

Power Pages Web API Testing

Test your Power Pages portal's table permissions, anonymous vs. authenticated access, and data operations

Power Pages exposes a subset of Dataverse operations through the Portals Web API. This runs in portal context and respects table permissions (web roles). Postman is useful for verifying whether your portal's permissions are configured correctly.

⚠️
Power Pages Web API ≠ Dataverse Web APIThe base URL is your portal URL (not the Dataverse org URL). Authentication uses portal session cookies — not the Dataverse OAuth token. Keep these as separate Postman environments.

GET — Retrieve Data via Power Pages Web API

Power Pages Web API — RetrieveGET
// Base URL: your portal URL + /_api/
URL: https://yourportal.powerappsportals.com/_api/contacts?$select=fullname,emailaddress1&$top=5

// Required Headers
Accept                    : application/json
Content-Type              : application/json
__RequestVerificationToken: <antiforgery_token>

// For authenticated portal sessions, pass the cookie:
Cookie: .AspNet.ApplicationCookie=<cookie_value>

// Get antiforgery token first:
// GET https://yourportal.powerappsportals.com/_api/antiforgery/token

POST — Create a Record via Power Pages Web API

Power Pages Web API — CreatePOST
URL: https://yourportal.powerappsportals.com/_api/cr123_customentities

// Headers
Content-Type              : application/json
__RequestVerificationToken: <antiforgery_token>

// Body
{
  "cr123_name":   "Test Record from Postman",
  "cr123_status": "Active"
}

// 201 Created  = success
// 403 Forbidden = table permission not granted for the web role
// 401 Unauthorized = portal login required
💡
Debug Table Permissions FasterWhen Power Pages returns 403, test the same request with different portal session cookies (different web roles) to identify which web role is missing the required table permission. Much faster than navigating the portal admin UI repeatedly.

6

Native Postman ↔ Power Automate Integration

Postman natively integrates with Power Automate — backup collections, send monitor results, and post activity feeds

In this scenario, Postman is the event source and Power Automate is the target. Postman sends events — collection changes, monitor results, team activity — into Power Automate flows via webhooks. Useful for team notification workflows and automated backups.

Use Case 1 — Auto-backup a Postman Collection

1
In Power Automate, create a flow with When an HTTP request is received trigger. Copy the generated webhook URL.
2
In Postman:Home → Integrations → Browse All Integrations → Search "Microsoft Power Automate"
3
Select"Back up a collection". Enter the Nickname, choose your Workspace, choose your Collection, paste the Notification URL (webhook). ClickAdd Integration.
4
Postman detects collection changes and sends them to Power Automate. Your flow can then save the backup to SharePoint, OneDrive, or any storage connector.

Use Case 2 — Monitor Results → Teams/Email Notification

JSON Schema for Monitor Results WebhookJSON
// Use this schema in your Power Automate HTTP trigger to parse Postman monitor payloads
{
  "type": "object",
  "properties": {
    "collection_uid": { "type": "string" },
    "monitor_name":   { "type": "string" },
    "run_id":         { "type": "string" },
    "status":         { "type": "string" },
    "failed_count":   { "type": "integer" },
    "passed_count":   { "type": "integer" },
    "run_at":         { "type": "string" }
  }
}

// In Power Automate after trigger:
// 1. Parse JSON using the schema above
// 2. Condition: if failed_count > 0
// 3. Send Teams notification OR email via Outlook connector

Use Case 3 — Team Activity → Email Notification

Integration Setup — Team ActivityConfig
// In Postman Integration setup for "Post collection activity":
Nickname         : MyTeam-PA-Integration
Choose Workspace : Your shared workspace name
Choose Collection: Your collection to monitor
Notification URL : <Power Automate webhook URL>

// Power Automate flow actions after trigger:
// Parse JSON → Send Outlook email with dynamic content:
//   "Collection @{body('Parse_JSON')?['collection_name']} was updated
//    by @{body('Parse_JSON')?['triggered_by']} at @{utcNow()}"

⚡ Common Errors & Quick Fixes

401 Unauthorized

Token expired or missing. Click Get New Access Token in Postman Auth tab. For tenant-restricted flows, verify your Entra app has Power Automate API permissions with admin consent.

403 Forbidden

Authenticated but not authorized. For Dataverse: user lacks the required security role. For Power Pages: table permission not assigned to the web role.

406 Not Acceptable

Wrong or missing Accept header. Add Accept: application/json + OData-MaxVersion: 4.0 + OData-Version: 4.0 to all Dataverse requests.

415 Unsupported Media Type

Missing Content-Type: application/json on POST/PATCH requests. Always include this when sending a JSON body.

404 Not Found

Entity set name wrong or record GUID doesn't exist. Verify the entity set name using {{webapiurl}}$metadata — it's plural and lowercase (e.g., contacts, not Contact).

MisMatchingOAuthClaims

Power Automate API scope URL has a single slash. Must use double slash: https://service.flow.microsoft.com//.default

Required Headers — Quick Reference

Headers Cheat SheetReference
// ── Dataverse Web API (all requests) ────────────────────────
Accept          : application/json
OData-MaxVersion: 4.0
OData-Version   : 4.0
Authorization   : Bearer {{accessToken}}

// ── Add for POST / PATCH (requests with a body) ──────────────
Content-Type    : application/json

// ── Add for PATCH to avoid 412 errors ────────────────────────
If-Match        : *

// ── Power Pages Web API ───────────────────────────────────────
Accept                    : application/json
Content-Type              : application/json
__RequestVerificationToken: <antiforgery_token>

// ── Power Automate HTTP Trigger ───────────────────────────────
Content-Type    : application/json
Accept          : application/json
Authorization   : Bearer <entra_token>   // only for tenant-restricted flows

Featured Post

How to Restore Deleted Emails in Exchange Online (Microsoft 365): The Complete Admin Guide

From the user's own "Recover deleted items" button to PowerShell bulk restores, and how to find out who deleted what, and whe...

Popular posts