Showing posts with label GraphAPI. Show all posts
Showing posts with label GraphAPI. Show all posts

Monday, June 22, 2026

Send Emails via Microsoft Graph API Using PowerShell and App Registration

Send Emails via Microsoft Graph API Using PowerShell and App Registration

Microsoft Graph API is the unified gateway to Microsoft 365 data and services. One of the most common automation scenarios is sending emails programmatically — without relying on a logged-in user, Outlook, or SMTP. In this post, we'll walk through how to register an Azure AD app, grant it the right permissions, and use a single end-to-end PowerShell script to send emails and query SharePoint via Graph API using client credentials (app-only auth).


Why Use Graph API for Sending Emails?

  • No user sign-in required — works great for background jobs, bots, and automation flows
  • Works from anywhere — PowerShell scripts, Power Automate Desktop, Azure Functions, etc.
  • Scalable and auditable — full control over sending identity and logging

Step 1: Register an App in Microsoft Entra ID (Azure AD)

Before writing any code, you need an app registration that represents your script or automation.

  1. Go to https://portal.azure.com
  2. Navigate to Microsoft Entra ID → App registrations → New registration
  3. Give it a meaningful name (e.g., GraphAPI-MailSender)
  4. Leave Redirect URI blank — not needed for client credentials flow
  5. Click Register

Once created, note down:

  • Application (client) ID → used as $clientId in the script
  • Directory (tenant) ID → used in the token endpoint URL

Step 2: Create a Client Secret

  1. Go to Certificates & secrets → New client secret
  2. Add a description and set an expiry (e.g., 12 months)
  3. Copy the Value immediately — it won't be shown again
  4. Store it securely (Azure Key Vault is recommended for production)

Step 3: Grant API Permissions

Your app needs the following Application permissions (not Delegated):

Permission Purpose
Mail.Send Send email as any mailbox in the tenant
Sites.Read.All Read SharePoint site data

Steps:

  1. Go to API permissions → Add a permission → Microsoft Graph → Application permissions
  2. Add Mail.Send and Sites.Read.All
  3. Click Grant admin consent — required for application permissions

⚠️ Mail.Send as an application permission allows sending mail as any user in the tenant. Use application access policies to restrict it to specific mailboxes in production.


Step 4: Full PowerShell Script

The script below handles everything in sequence:

  1. Acquires a token using client credentials
  2. Sends an email via Graph API
  3. Reuses the token to query the SharePoint root site
# ============================================================
# Microsoft Graph API — Send Email + Query SharePoint Root Site
# Using App-Only (Client Credentials) Authentication
# ============================================================

# ── Configuration ──────────────────────────────────────────
$clientId     = "<YOUR_CLIENT_ID>"       # Application (client) ID
$clientSecret = "<YOUR_CLIENT_SECRET>"   # Client secret value
$tenantId     = "<YOUR_TENANT_ID>"       # Directory (tenant) ID
$senderUPN    = "sender@yourdomain.com"  # Mailbox used to send email
$recipientUPN = "recipient@yourdomain.com" # Target recipient

# ── Part 1: Acquire Access Token ────────────────────────────
Write-Output "Acquiring access token..."

$tokenBody = @{
    client_id     = $clientId
    client_secret = $clientSecret
    scope         = "https://graph.microsoft.com/.default"
    grant_type    = "client_credentials"
}

$tokenResponse = Invoke-RestMethod `
    -Uri    "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
    -Method POST `
    -Body   $tokenBody

$token = $tokenResponse.access_token
Write-Output "Token acquired successfully."

# ── Part 2: Send Email via Graph API ────────────────────────
Write-Output "Sending email..."

$mailPayload = @{
    message = @{
        subject = "Test Email from Graph API"
        body    = @{
            contentType = "Text"
            content     = "This is a test email sent via Microsoft Graph API using PowerShell with app-only authentication."
        }
        toRecipients = @(
            @{
                emailAddress = @{
                    address = $recipientUPN
                }
            }
        )
    }
} | ConvertTo-Json -Depth 10

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

try {
    Invoke-RestMethod `
        -Uri         "https://graph.microsoft.com/v1.0/users/$senderUPN/sendMail" `
        -Method      POST `
        -Headers     $headers `
        -Body        $mailPayload `
        -ContentType "application/json"

    Write-Output "✅ Email sent successfully to $recipientUPN"
}
catch {
    Write-Error "❌ Failed to send email: $_"
}

# ── Part 3: Query SharePoint Root Site ──────────────────────
Write-Output "`nQuerying SharePoint root site..."

try {
    $rootSite = Invoke-RestMethod `
        -Uri     "https://graph.microsoft.com/v1.0/sites/root" `
        -Method  GET `
        -Headers $headers

    Write-Output "✅ SharePoint root site retrieved:"
    Write-Output ($rootSite | ConvertTo-Json -Depth 3)
}
catch {
    Write-Error "❌ Failed to query SharePoint root site: $_"
}

How to Run the Script

  1. Open PowerShell (or Windows PowerShell ISE / VS Code)
  2. Replace the four placeholder values at the top of the script:
    • <YOUR_CLIENT_ID>
    • <YOUR_CLIENT_SECRET>
    • <YOUR_TENANT_ID>
    • sender@yourdomain.com and recipient@yourdomain.com
  3. Save as Send-GraphEmail.ps1
  4. Run:
.\Send-GraphEmail.ps1

Expected Output

Acquiring access token...
Token acquired successfully.
Sending email...
✅ Email sent successfully to recipient@yourdomain.com

Querying SharePoint root site...
✅ SharePoint root site retrieved:
{
  "id": "yourtenant.sharepoint.com,xxxxxxxx-...",
  "displayName": "Communication site",
  "name": "root",
  "webUrl": "https://yourtenant.sharepoint.com"
}

Script Breakdown

Section What it does
Configuration block Centralises all credentials and addresses at the top — easy to maintain
Token acquisition Posts to the OAuth 2.0 token endpoint using client credentials
Email sending Constructs the mail JSON payload and calls /users/{sender}/sendMail
SharePoint query Reuses the same token to call /sites/root — no second token needed
try/catch blocks Catches errors per operation so one failure doesn't stop the rest

Security Best Practices

Practice Recommendation
Never hardcode secrets Move $clientSecret to Azure Key Vault or environment variables
Rotate secrets regularly Set reminders before expiry; automate via Key Vault rotation
Restrict Mail.Send scope Use application access policies to limit to specific mailboxes
Prefer certificates Certificate-based auth (New-SelfSignedCertificate) is more secure than secrets
Audit permissions Review app registrations and permissions in Entra ID quarterly

Summary

In this post, we covered:

  • Registering an app in Microsoft Entra ID and creating a client secret
  • Granting Mail.Send and Sites.Read.All application permissions with admin consent
  • A complete PowerShell script that acquires a token, sends an email, and queries SharePoint — all using app-only auth
  • Error handling with try/catch per operation
  • Security recommendations for production use

This pattern is perfect for Power Automate Desktop flows, scheduled PowerShell jobs, or any scenario where no interactive user is present. The same token works across multiple Graph API endpoints — making it efficient to chain operations in a single script.


Tags: MicrosoftGraph, PowerShell, AzureAD, AppRegistration, Microsoft365, EmailAutomation, GraphAPI, EntraID, PowerAutomate, SharePoint


Part 2: End-to-End in Power Automate Desktop (PAD)

The same logic — get a token, send an email, query SharePoint — can run entirely inside a Power Automate Desktop flow without any external PowerShell script. PAD has built-in HTTP actions and a Run PowerShell script action, giving you two clean approaches.


Approach A: Using the "Run PowerShell Script" Action (Quickest Way)

This embeds the entire PowerShell script directly inside a PAD flow. Best when you already have the script and just want to automate it.

PAD Flow Structure

Flow: Send Email via Graph API
│
├── [1] Set Variable — clientId
├── [2] Set Variable — clientSecret
├── [3] Set Variable — tenantId
├── [4] Set Variable — senderUPN
├── [5] Set Variable — recipientUPN
├── [6] Run PowerShell Script
│       └── Script: (full script using %clientId%, %clientSecret%, etc.)
│       └── Output: PowershellOutput
├── [7] IF PowershellOutput contains "successfully"
│       └── Display Message — "Flow completed successfully"
│   ELSE
│       └── Display Message — "Flow encountered an error"
└── [8] (Optional) Write output to text file or log

Step-by-Step in PAD Designer

Step 1 — Set your variables

Add one Set variable action for each credential. Using variables (rather than hardcoding) keeps the script clean and makes future updates easy.

Variable Name Value
clientId Your Application (client) ID
clientSecret Your client secret value
tenantId Your Directory (tenant) ID
senderUPN sender@yourdomain.com
recipientUPN recipient@yourdomain.com

In PAD, go to Actions panel → Variables → Set variable, set Name and Value for each.

Step 2 — Add "Run PowerShell Script" action

Search for Run PowerShell script in the Actions panel (under Scripting). Paste the script below into the script body. PAD variables are referenced using %variableName% syntax inside the script block.

# ============================================================
# Graph API — Send Email + Query SharePoint Root Site
# Running inside Power Automate Desktop
# ============================================================

$clientId     = "%clientId%"
$clientSecret = "%clientSecret%"
$tenantId     = "%tenantId%"
$senderUPN    = "%senderUPN%"
$recipientUPN = "%recipientUPN%"

# ── Part 1: Acquire Token ────────────────────────────────────
$tokenBody = @{
    client_id     = $clientId
    client_secret = $clientSecret
    scope         = "https://graph.microsoft.com/.default"
    grant_type    = "client_credentials"
}

$tokenResponse = Invoke-RestMethod `
    -Uri    "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" `
    -Method POST `
    -Body   $tokenBody

$token = $tokenResponse.access_token

# ── Part 2: Send Email ───────────────────────────────────────
$mailPayload = @{
    message = @{
        subject = "Test Email from Graph API via PAD"
        body    = @{
            contentType = "Text"
            content     = "This email was sent via Microsoft Graph API running inside a Power Automate Desktop flow."
        }
        toRecipients = @(
            @{ emailAddress = @{ address = $recipientUPN } }
        )
    }
} | ConvertTo-Json -Depth 10

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

try {
    Invoke-RestMethod `
        -Uri         "https://graph.microsoft.com/v1.0/users/$senderUPN/sendMail" `
        -Method      POST `
        -Headers     $headers `
        -Body        $mailPayload `
        -ContentType "application/json"

    Write-Output "Email sent successfully to $recipientUPN"
}
catch {
    Write-Output "ERROR sending email: $_"
}

# ── Part 3: Query SharePoint Root Site ──────────────────────
try {
    $rootSite = Invoke-RestMethod `
        -Uri     "https://graph.microsoft.com/v1.0/sites/root" `
        -Method  GET `
        -Headers $headers

    Write-Output "SharePoint root site: $($rootSite.webUrl)"
}
catch {
    Write-Output "ERROR querying SharePoint: $_"
}

In the action settings:

  • Script to run → paste the script above
  • PowerShell script output → save to variable PowershellOutput
  • Script error output → save to variable ScriptError

Step 3 — Handle output with an IF condition

Add an If action:

  • First operand: %PowershellOutput%
  • Operator: Contains
  • Second operand: successfully

Inside the If block → add Display message: "✅ Email sent and SharePoint queried successfully"
Inside the Else block → add Display message: "❌ Flow error — check ScriptError variable"

Step 4 — (Optional) Log output to a file

Add a Write text to file action after the If block:

  • File path: C:\PAD-Logs\GraphAPI-Run-%CurrentDateTime%.txt
  • Text to write: %PowershellOutput%%NewLine%%ScriptError%
  • If file exists: Append

Approach B: Using PAD HTTP Actions (No PowerShell Required)

PAD has native HTTP actions under Web → Invoke web service that can call REST APIs directly — no PowerShell needed. This is the cleanest approach for PAD-native flows.

PAD Flow Structure

Flow: Send Email via Graph API (HTTP Native)
│
├── [1]  Set Variable — clientId
├── [2]  Set Variable — clientSecret  
├── [3]  Set Variable — tenantId
├── [4]  Set Variable — senderUPN
├── [5]  Set Variable — recipientUPN
│
├── [6]  Invoke web service — POST token endpoint
│        └── Saves response → TokenResponse (JSON)
│
├── [7]  Get JSON key — extract access_token
│        └── Saves → AccessToken
│
├── [8]  Invoke web service — POST sendMail
│        └── Uses Bearer %AccessToken% in header
│        └── Saves response → MailResult
│
├── [9]  Invoke web service — GET sites/root
│        └── Uses Bearer %AccessToken% in header
│        └── Saves response → SharePointResult
│
└── [10] Display message — show results

Step-by-Step: HTTP Actions in PAD

Step 1 — Set variables (same as Approach A)

Step 2 — Get Access Token (Invoke web service)

Action: Web → Invoke web service

Field Value
URL https://login.microsoftonline.com/%tenantId%/oauth2/v2.0/token
Method POST
Accept application/json
Content type application/x-www-form-urlencoded
Custom headers (leave blank)
Request body client_id=%clientId%&client_secret=%clientSecret%&scope=https://graph.microsoft.com/.default&grant_type=client_credentials
Save response into TokenResponse

Step 3 — Extract the access token

Action: Variables → Get JSON key value
(or use Convert JSON to custom object then dot-notation)

  • JSON: %TokenResponse%
  • Key: access_token
  • Save into: AccessToken

Alternatively, use a Run PowerShell script just for this one-liner:

$json = '%TokenResponse%' | ConvertFrom-Json
Write-Output $json.access_token

Save output into AccessToken.

Step 4 — Send Email (Invoke web service)

Action: Web → Invoke web service

Field Value
URL https://graph.microsoft.com/v1.0/users/%senderUPN%/sendMail
Method POST
Accept application/json
Content type application/json
Custom headers Authorization: Bearer %AccessToken%
Request body (see JSON below)
Save response into MailResult

Request body (paste as-is, update addresses via variables):

{
  "message": {
    "subject": "Test Email from PAD via Graph API",
    "body": {
      "contentType": "Text",
      "content": "This email was sent using Power Automate Desktop with Microsoft Graph API HTTP actions."
    },
    "toRecipients": [
      {
        "emailAddress": {
          "address": "%recipientUPN%"
        }
      }
    ]
  }
}

Step 5 — Query SharePoint Root Site (Invoke web service)

Action: Web → Invoke web service

Field Value
URL https://graph.microsoft.com/v1.0/sites/root
Method GET
Accept application/json
Custom headers Authorization: Bearer %AccessToken%
Save response into SharePointResult

Step 6 — Display results

Action: Message boxes → Display message
Message: SharePoint site: %SharePointResult%


Approach Comparison

Approach A (Run PowerShell) Approach B (HTTP Actions)
Complexity Low — paste and run Medium — multiple actions
PAD native No (calls PowerShell) Yes (pure PAD)
Debugging Via script output variable Per-action response variables
Best for Existing PowerShell scripts New PAD-native flows
Token parsing Built into the script Needs extra step in PAD
Error handling try/catch in script On block error / IF conditions

Recommendation: Use Approach A if you already have the PowerShell script. Use Approach B if you want a fully PAD-native flow with no external dependencies.


Tips for PAD Flows with Graph API

  • Store secrets in PAD Input variables marked as Sensitive — they are masked in logs and not stored in plain text in the flow definition
  • Add On block error handlers around each Invoke web service action to gracefully catch HTTP failures (401 Unauthorized, 403 Forbidden, etc.)
  • Token expiry: The client credentials token is valid for 1 hour. For long-running flows, re-acquire the token mid-flow if needed
  • Test in PAD debugger using the step-through (F10) to inspect each variable value before running end-to-end

Full Flow Summary

App Registration (Entra ID)
        │
        ▼
Client Credentials → OAuth 2.0 Token Endpoint
        │
        ▼
Access Token (Bearer)
        │
        ├──▶ POST /users/{sender}/sendMail   → Email delivered ✅
        │
        └──▶ GET  /sites/root               → SharePoint data ✅

All three entry points — standalone PowerShell, PAD via PowerShell action, and PAD via HTTP actions — use the exact same app registration, permissions, and token. The only difference is where and how the script runs.


Tags: MicrosoftGraph, PowerShell, PowerAutomateDesktop, PAD, AzureAD, AppRegistration, Microsoft365, EmailAutomation, GraphAPI, EntraID, SharePoint, RPA

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.



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