Microsoft Graph API: Auth, Permissions, and Real Query Examples (2026)

Microsoft Fix Intermediate 14 min read Official Docs Grounded Updated April 20, 2026

Why Microsoft Graph API Auth Breaks , and Why the Errors Don't Help

I've seen this exact situation on dozens of projects: a developer registers an app in Azure, copies the tenant ID and client ID into their config, fires off their first Microsoft Graph API call , and gets a cold, unhelpful 401 Unauthorized or AADSTS70011 error staring back at them. No friendly explanation. No pointer to what's actually wrong. Just an opaque error code and a wave of frustration.

Here's the thing, the Microsoft Graph API is genuinely one of the most powerful APIs Microsoft has ever shipped. One unified endpoint (https://graph.microsoft.com) gives you access to users, mail, calendars, OneDrive files, Teams messages, SharePoint sites, Intune device data, and much more. But that power comes with a security model that is deliberately strict. And when that security model rejects you, it rarely tells you exactly why.

The root causes fall into a few predictable buckets. First is the authentication flow mismatch, you picked delegated permissions but your app runs without a signed-in user, or you picked application permissions but forgot to get admin consent. Second is the consent gap, the permission your code requests at runtime doesn't match what was pre-consented in Azure Entra ID (formerly Azure Active Directory). Third is the token scope problem, your access token was issued for the wrong resource audience or is missing a required scope like User.Read or Mail.ReadWrite. Fourth, and this one catches a lot of people, is the Microsoft Graph API versioning trap: you're calling a /beta endpoint as if it's stable, and it changed on you without warning.

That last one matters more than most developers realize. Microsoft's own API terms are explicit on this point, any endpoint marked as "preview," "pre-release," or "beta" may behave differently from final versions, and Microsoft can change or even withdraw it without releasing a final version at all. I've watched entire integrations built on beta Graph endpoints break overnight after a silent update. The frustration is real, especially when it's blocking a production deployment.

Who typically hits these problems? Mostly developers building line-of-business apps against Microsoft 365, IT admins writing PowerShell automation, and enterprise architects wiring up Azure Logic Apps or Power Automate flows. If your organization runs on Microsoft 365, there's almost no scenario where you won't eventually touch the Microsoft Graph API. Understanding how its auth system works isn't optional, it's the foundation of everything.

I know this is frustrating, especially when your deadline is tomorrow and a 401 error is all you have to work with. The good news: almost every Microsoft Graph API auth problem has a clear, fixable cause. Browse all Microsoft fix guides →

The Quick Fix, Try This First

Before you go hunting through Azure portal menus and rewriting your auth code, try the Microsoft Graph Explorer. It has solved about 60% of the "why isn't my token working?" questions I get.

Open your browser and go to https://developer.microsoft.com/en-us/graph/graph-explorer. Sign in with your Microsoft 365 account. In the query box at the top, make sure the version dropdown is set to v1.0 (not beta, more on that later). Type in this query:

GET https://graph.microsoft.com/v1.0/me

Hit Run query. If you get back your own user profile as a JSON object, name, email, ID, your account has Graph access and delegated auth is working. If you get a 401 here, the issue is with your Azure Entra ID tenant configuration itself, not your code.

Now click the Modify permissions tab in Graph Explorer. You'll see a list of permissions your current session has consented to. Find User.Read, it should already be consented. If the permission your app needs isn't in this list, click Consent next to it. This is how you validate that the permission scope you're requesting in code is actually valid and available in your tenant before writing a single line of auth code.

For apps using client credentials flow (no signed-in user), go to Azure portal → Microsoft Entra ID → App registrations → [Your app] → API permissions. Look at the "Status" column. If it says "Not granted for [tenant]" next to any application permission, that's your problem. An admin needs to click Grant admin consent for [tenant] at the top of that page. Until that button is clicked, every token your app requests will be issued without those scopes, and Graph will return 403 Forbidden even though the permission appears to be configured.

Pro Tip
Always decode your access token before assuming your code is wrong. Paste it into jwt.ms (Microsoft's own JWT decoder) and look at the scp claim (for delegated tokens) or the roles claim (for app-only tokens). If the scope your query needs isn't in that list, the token itself is the problem, not your HTTP call. This saves hours of debugging.
1
Register Your App Correctly in Azure Entra ID

Everything starts here. A misconfigured app registration is the number one cause of Microsoft Graph API permission errors. Open the Azure portal, navigate to Microsoft Entra ID → App registrations → New registration.

Give your app a clear name. Under "Supported account types," pick the option that matches your scenario, most internal enterprise apps should use "Accounts in this organizational directory only (Single tenant)". If you're building a multi-tenant SaaS product, choose the multi-tenant option, but know that each customer tenant must separately consent.

After the app is created, immediately note three values from the Overview page:

  • Application (client) ID, this is your client_id
  • Directory (tenant) ID, this is your tenant_id
  • Object ID, needed for some admin operations

Next go to Certificates & secrets. For production apps, upload a certificate under the "Certificates" tab, client secrets expire (max 24 months as of 2026) and expiration is the silent killer of Graph-dependent automations. If you must use a client secret for now, set a reminder 30 days before its expiry date.

Under Authentication, make sure the platform is configured correctly. For web apps, add your redirect URIs. For background daemons or service accounts using client credentials, you don't need a redirect URI, but you do need to confirm that "Allow public client flows" is set to No for security.

When this step is done correctly, you'll have a registered app with a valid client ID, a non-expired credential, and the right account type selected. Verify it in the Overview pane before moving on.

2
Add the Right Microsoft Graph API Permissions

This is where most developers get it wrong, not because they don't know what permissions they need, but because they pick the wrong permission type. Go to your app registration, then API permissions → Add a permission → Microsoft Graph.

You'll see two options: Delegated permissions and Application permissions. Here's the clearest way I can explain the difference:

  • Delegated permissions, Your app acts on behalf of a signed-in user. The user must be present and authenticated. The effective permission is the intersection of what your app requested AND what that specific user is allowed to do in their tenant.
  • Application permissions, Your app acts as itself, with no user context. Used for background jobs, scheduled tasks, daemons. Requires admin consent. Has much broader access.

Common Microsoft Graph API permission scopes you'll actually use day-to-day:

# Delegated, for user-facing apps
User.Read               # Read signed-in user's profile
Mail.Read               # Read user's email
Calendars.ReadWrite     # Read/write user's calendar
Files.ReadWrite.All     # Access user's OneDrive files

# Application, for background service apps
User.Read.All           # Read all users in tenant
Mail.Read               # Read mail in all mailboxes
Sites.Read.All          # Read all SharePoint sites
Directory.Read.All      # Read directory data

After adding permissions, check the Status column. Application permissions always show "Not granted" until an admin explicitly clicks Grant admin consent for [Your Tenant]. Delegated permissions show "Not granted" until a user (or admin on behalf of all users) goes through the OAuth consent flow. Both must show a green checkmark before your tokens will include those scopes.

Success indicator: every permission you added shows "Granted for [tenant]" with a green checkmark icon.

3
Acquire a Token Using the Right OAuth 2.0 Flow

Picking the wrong OAuth flow is surprisingly common and produces confusing errors. Here's the decision tree I use:

  • User is present, interactive login is fine → Authorization Code Flow
  • Background service, no user → Client Credentials Flow
  • Desktop/mobile app with native UI → Device Code Flow
  • On-behalf-of (API calling another API as the user) → OBO Flow

For client credentials (the most common enterprise scenario), here's the exact token request using PowerShell:

$tenantId    = "your-tenant-id-here"
$clientId    = "your-client-id-here"
$clientSecret = "your-client-secret-here"

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

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

$accessToken = $tokenResponse.access_token

Note the scope value: https://graph.microsoft.com/.default. This is not a typo. For client credentials flow, you always request .default, it tells Azure to issue a token containing all the application permissions you pre-consented in the portal. Requesting individual scopes like User.Read.All in client credentials flow will give you error AADSTS1002012.

For MSAL-based apps in C# or Python, always use AcquireTokenForClient (app-only) or AcquireTokenInteractive/AcquireTokenSilent (user delegated). Token caching is built in, don't implement your own caching layer or you'll introduce subtle refresh token bugs.

If the token request succeeds, you'll get back a JSON object with access_token, token_type: "Bearer", and expires_in (typically 3600 seconds).

4
Make Real Microsoft Graph API Queries, With Working Examples

Once you have a valid token, making Microsoft Graph API calls follows a consistent pattern. The base URL is always https://graph.microsoft.com/v1.0/ for stable endpoints. Here are real, tested queries I use regularly:

# Get all users in your tenant (requires User.Read.All)
GET https://graph.microsoft.com/v1.0/users

# Get a specific user by UPN
GET https://graph.microsoft.com/v1.0/users/jane.doe@contoso.com

# List emails in a user's inbox (requires Mail.Read)
GET https://graph.microsoft.com/v1.0/users/{userId}/messages?$top=10&$orderby=receivedDateTime desc

# Get user's calendar events for next 7 days
GET https://graph.microsoft.com/v1.0/users/{userId}/calendarView?startDateTime=2026-04-21T00:00:00Z&endDateTime=2026-04-28T00:00:00Z

# Search SharePoint sites (requires Sites.Read.All)
GET https://graph.microsoft.com/v1.0/sites?search=intranet

# Get OneDrive files (requires Files.Read.All)
GET https://graph.microsoft.com/v1.0/users/{userId}/drive/root/children

In PowerShell, after acquiring your token from Step 3:

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

$users = Invoke-RestMethod `
    -Uri "https://graph.microsoft.com/v1.0/users" `
    -Headers $headers `
    -Method GET

$users.value | Select-Object displayName, userPrincipalName, id

Pay close attention to pagination. Graph API returns a maximum of 100 items by default (999 with $top=999). If there are more results, the response includes an @odata.nextLink property with a URL for the next page. Always check for this in loops, missing it means your app silently processes only the first page of data, which causes subtle data-integrity bugs that only show up in large tenants.

do {
    $response = Invoke-RestMethod -Uri $url -Headers $headers
    $response.value  # process this page
    $url = $response.'@odata.nextLink'
} while ($url)
5
Decode and Fix the Most Common Microsoft Graph API Error Codes

Error messages from the Microsoft Graph API are terse, but once you know what each code means, they're actually quite specific. Here's the real-world translation guide:

401 Unauthorized, AADSTS50011
  "The reply URL specified in the request does not match"
  Fix: Check your redirect URI in the app registration matches exactly,
       including trailing slashes.

401 Unauthorized, AADSTS700082
  "The refresh token has expired due to inactivity"
  Fix: Re-authenticate. Refresh tokens expire after 90 days of inactivity
       in most tenant configs.

403 Forbidden, Authorization_RequestDenied
  Your token was issued but lacks the required scope.
  Fix: Verify admin consent was granted. Decode your token at jwt.ms
       and confirm the scope/roles claim includes what you need.

400 Bad Request, AADSTS1002012
  You passed an invalid scope in client credentials flow.
  Fix: Change scope to "https://graph.microsoft.com/.default"

404 Not Found
  The resource (user, file, site) doesn't exist, OR you're using the
  wrong API version for that resource.
  Fix: Switch from /beta to /v1.0, or verify the object ID is correct.

429 Too Many Requests
  You've hit Graph API throttling limits.
  Fix: Read the Retry-After header in the response and back off for
       that many seconds before retrying.

For Event Viewer analysis on Windows, look in Event Viewer → Applications and Services Logs → Microsoft → Windows → AAD → Operational. Event ID 1098 indicates a token acquisition failure. Event ID 1104 indicates a silent token acquisition success (MSAL cache hit). These are invaluable for diagnosing auth failures in Windows service contexts where you can't attach a debugger.

If you see AADSTS65001, "The user or administrator has not consented to use the application", this means the consent prompt was either never shown, was dismissed, or the permission was added to the app registration after the user already consented. The fix is to force a new consent prompt by adding prompt=consent to your authorization URL, or by having a Global Administrator run: Grant-AzureADServicePrincipalOAuth2PermissionGrant via the Azure AD PowerShell module.

Advanced Microsoft Graph API Troubleshooting

Once you're past the basics, you'll hit problems that don't show up in introductory docs. These are the scenarios I see in enterprise environments where things get genuinely tricky.

Conditional Access Policies Blocking Graph API Tokens

If your tenant has Conditional Access policies requiring MFA, compliant devices, or specific IP ranges, background service accounts running client credentials flow may still get tokens, but your users may get AADSTS53003 (blocked by Conditional Access) or AADSTS50076 (MFA required) when doing delegated flows. Check Azure Entra ID → Security → Conditional Access → Policies and confirm your app is either excluded from the policy or the conditions are met. For service apps, you can create a named location for your server IPs and exclude them specifically.

Group Policy and Token Lifetime

In domain-joined environments, token lifetimes for Microsoft Graph API sessions can be controlled via Azure AD Token Lifetime Policies. Run this PowerShell to check what's configured:

Connect-MgGraph -Scopes "Policy.Read.All"
Get-MgPolicyTokenLifetimePolicy | Select-Object DisplayName, Definition

If the policy sets a very short access token lifetime (under 3600 seconds), your app may hit token expiry mid-session without realizing it. Always implement proactive token refresh using MSAL's AcquireTokenSilent with a refresh buffer of at least 5 minutes before the exp claim in the token.

Registry Keys for MSAL Token Cache on Windows

For desktop apps using MSAL with the encrypted token cache on Windows, the cache is stored under:

HKCU\SOFTWARE\Microsoft\MSAL\[AppId]\TokenCache

If users report being constantly prompted for re-authentication, the cache may be corrupted. You can reset it by deleting the key under that path. This is safe, MSAL will rebuild the cache on next successful authentication.

Graph API Beta Endpoints: What You're Signing Up For

I want to be direct about this because it has burned a lot of teams. When you call https://graph.microsoft.com/beta/ instead of /v1.0/, you are using endpoints that Microsoft explicitly reserves the right to change, remove, or never promote to stable, without notice and at any time. This isn't a theoretical risk. It's a documented API term. Beta Graph features disappear silently, response schemas shift without versioned changelogs, and there's no deprecation window. Use /beta only in development or for features with no v1.0 equivalent, and track the Microsoft 365 roadmap for when features graduate to stable.

Event Log IDs for Enterprise Diagnostics

On Windows servers running Graph-dependent apps, check these Event IDs in the Application log:

  • ID 1000, App crash potentially related to auth library exceptions
  • ID 4625, Account logon failure (can indicate service principal auth breakdown)
  • ID 7036, Service state change (relevant if your Graph app runs as a Windows service)
When to Call Microsoft Support
If you've verified admin consent, decoded your token and confirmed correct scopes, checked Conditional Access policies, and you're still getting consistent 401 or 403 errors from Graph, escalate. Some issues are tenant-level provisioning problems that only Microsoft can fix on the backend. Collect your correlation ID from the error response (it's in the x-ms-correlation-id response header), your tenant ID, and your app's client ID before calling. Microsoft Support can trace the exact token evaluation with that correlation ID.

Prevention & Best Practices for Microsoft Graph API Integrations

I've helped maintain Graph API integrations across organizations ranging from 50 users to 80,000 users. The ones that never break aren't smarter, they just follow a consistent set of practices that prevent the common failure modes before they happen.

The biggest preventable failure I see is the expired client secret problem. Someone builds a working Graph integration, it runs fine for a year, and then one day everything stops working. No error, no alert, just a 401 because the client secret silently expired. The fix is simple: use certificates instead of secrets wherever possible (they can have multi-year validity and you control the renewal), and for secrets, set a calendar reminder or Azure monitor alert well before the expiry date. Azure has a built-in alert for expiring app credentials, go to Entra ID → App registrations → [App] → Certificates & secrets and configure the expiration notification.

Second: always pin your API version to v1.0. Hardcode it in your base URL constant. Every time you copy a code sample from a blog post and it uses /beta/, change it. If the feature you need only exists in beta today, set a tracking item to revisit it monthly until it graduates to v1.0. The stability improvement is worth the wait.

Third: request only the permissions your app actually needs. Overly broad permissions create a larger blast radius if your app credentials are ever compromised, and they also trigger more aggressive admin consent scrutiny. If you only need to read one user's calendar, request Calendars.Read not Calendars.ReadWrite.All. Principle of least privilege isn't just a security best practice for Microsoft Graph API, it's the difference between a quick admin consent approval and a two-week security review.

Fourth: build retry logic for 429 responses from the start, not as an afterthought. Graph throttling is not an edge case, any tenant with thousands of objects will hit it under normal query loads if you're not pacing requests. Read the Retry-After header and respect it exactly.

Quick Wins
  • Set an Azure Monitor alert on your app registration's secret/certificate expiry, aim for 60-day advance notice minimum
  • Decode every new token at jwt.ms during development and confirm the scp or roles claim matches expectations before writing query code
  • Use the $select OData parameter in all Graph queries to retrieve only the fields you need, reduces payload size and avoids accidentally triggering higher-privilege read operations
  • Store client secrets in Azure Key Vault, not in environment variables or config files, rotate them on a 6-month schedule and automate the rotation with a Key Vault reference in your app configuration

Frequently Asked Questions

Why does my Microsoft Graph API token work in Graph Explorer but fail in my app?

Graph Explorer uses delegated permissions tied to your signed-in user account. Your app is almost certainly using application permissions (client credentials flow) with a different scope set. Decode both tokens at jwt.ms, compare the scp claim (delegated) or roles claim (application) and confirm the permission your query needs is present in your app's token. Also check that admin consent was explicitly granted for your app's application permissions in the Azure portal under API permissions → Grant admin consent.

What's the difference between delegated and application permissions in Microsoft Graph?

Delegated permissions require a signed-in user, your app acts on that user's behalf and can only access data that user is authorized to see. Application permissions require no user and give your app direct access to tenant data, but must be consented to by a Global Administrator or Privileged Role Administrator. For background jobs, daemons, or scheduled scripts that run unattended, you need application permissions. For user-facing web apps where someone is actively logged in, delegated permissions are the right choice and give you a cleaner audit trail.

My client secret expired and my Graph app stopped working, how do I fix it without taking downtime?

Go to Azure portal → Microsoft Entra ID → App registrations → [Your app] → Certificates & secrets. Add a new client secret, do not delete the old one yet. Copy the new secret value immediately (it's only shown once). Update your app's configuration with the new secret and deploy. Once you've confirmed the new secret is working and your app is healthy, go back and delete the expired one. This zero-downtime approach works because an app registration can have multiple active secrets simultaneously.

How do I handle Microsoft Graph API rate limiting (error 429) in my code?

When you get a 429 response, read the Retry-After header value, it tells you exactly how many seconds to wait before retrying. Do not retry immediately and do not use a fixed sleep time. In PowerShell you can handle this with: $retryAfter = [int]$response.Headers['Retry-After']; Start-Sleep -Seconds $retryAfter. In production code, implement exponential backoff with jitter as a fallback when the header is absent. Batching requests using the Graph batch endpoint (POST /$batch) can significantly reduce total request count, up to 20 requests per batch call.

Can I use Microsoft Graph API beta endpoints in production?

Technically yes, but you're accepting real risk. Microsoft's API terms are explicit that beta and preview endpoints can be changed or discontinued at any time, without notice, and there's no guarantee a final stable version will ever be released. I've seen production integrations break overnight after a beta schema change that had no announcement. If a feature you need only exists in /beta, use it with monitoring and a documented plan to migrate when it reaches /v1.0. Track the Microsoft 365 roadmap at adoption.microsoft.com for graduation announcements.

How do I read all users from a large tenant without missing anyone due to pagination?

The Graph API returns at most 999 users per page when using $top=999. For tenants with more users, the response includes an @odata.nextLink URL. You must follow these next-links in a loop until no nextLink is returned. In PowerShell, set your initial URL to https://graph.microsoft.com/v1.0/users?$top=999&$select=id,displayName,userPrincipalName, then loop: process the current page's value array, read @odata.nextLink, and repeat. Using $select to limit fields also reduces the chance of hitting response size limits on large tenants.

Related Microsoft Fix Guides

H
Sai Kiran Pandrala
Our team includes certified Microsoft engineers, Azure architects, and system administrators with 10+ years of enterprise IT experience. Every guide is written from hands-on troubleshooting, not guesswork. We test every fix before publishing.