Zia.
Microsoft Office Add-insJuly 8, 20266 min read

Microsoft Graph API: A Practical Guide for Office 365 Developers

A practical Microsoft Graph API guide for Office 365 developers — auth with MSAL, delegated vs application permissions, calling mail/calendar/users, pagination, and add-in SSO, with working code.

Microsoft Graph API: A Practical Guide for Office 365 Developers

Microsoft Graph is the single REST API that sits in front of all of Microsoft 365 — mail, calendar, contacts, users, groups, Teams, OneDrive, and more. One base URL, one auth model, one query language. If you're building anything that reads or writes Microsoft 365 data — an Outlook add-in, an internal dashboard, an automation — Graph is the door you go through.

This guide is the practical version: how auth actually works, the difference that trips everyone up, and real requests you can run today.

The one URL to know

Every Graph call goes to:

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

Append a resource path and you're talking to Microsoft 365. A few that cover most real work:

Endpoint Returns
/me The signed-in user's profile
/me/messages The user's email
/me/events Calendar events
/users All users in the tenant (admin)
/me/drive/root/children Files in OneDrive

The hard part isn't the URLs — it's getting a valid access token to send with them.

Authentication: MSAL and access tokens

Graph uses OAuth 2.0. You never send passwords; you send a short-lived bearer token that proves who's asking and what they're allowed to do:

Authorization: Bearer eyJ0eXAiOiJKV1Qi...

To get that token you use MSAL (Microsoft Authentication Library) and an app registration in Entra ID (formerly Azure AD). Register your app once in the Entra portal, note the client ID and tenant ID, and MSAL handles the token dance.

The critical decision — and the thing that confuses almost everyone at first — is which kind of permission your app uses.

Delegated vs application permissions

This distinction determines everything about how your app behaves:

  • Delegated permissions — the app acts on behalf of a signed-in user. It can only see what that user can see. Use this for anything with a user present: an add-in, a web app someone logs into. Scope example: Mail.Read.
  • Application permissions — the app acts as itself, with no user, typically across the whole tenant. Use this for background daemons and server-to-server jobs. It requires admin consent. Scope example: Mail.Read (application).

Rule of thumb: if a human is logged in, use delegated permissions. If it's a cron job running at 3 a.m., use application permissions. Choosing wrong is the number one reason Graph calls return 403 Forbidden.

Your first call (delegated, in the browser)

Here's a minimal browser flow using MSAL: sign the user in, get a token scoped to read their profile, and call /me.

import { PublicClientApplication } from "@azure/msal-browser";

const msal = new PublicClientApplication({
  auth: {
    clientId: "YOUR_CLIENT_ID",
    authority: "https://login.microsoftonline.com/YOUR_TENANT_ID",
    redirectUri: window.location.origin,
  },
});
await msal.initialize();

async function getMe() {
  const account = msal.getAllAccounts()[0] ?? undefined;
  const { accessToken } = await msal.acquireTokenSilent({
    scopes: ["User.Read"],
    account,
  }).catch(() => msal.acquireTokenPopup({ scopes: ["User.Read"] }));

  const res = await fetch("https://graph.microsoft.com/v1.0/me", {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  return res.json();
}

Notice the pattern: acquire token silently, fall back to interactive. MSAL caches tokens and refreshes them, so the popup only appears when it genuinely must.

Querying: $select, $filter, and pagination

Graph supports OData query parameters, and using them is the difference between a fast app and a slow one. Ask only for the fields you need:

const url =
  "https://graph.microsoft.com/v1.0/me/messages" +
  "?$select=subject,from,receivedDateTime" +
  "&$top=10" +
  "&$orderby=receivedDateTime desc";

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${accessToken}` },
});
const { value } = await res.json();

Graph pages large result sets. When there's more data, the response includes an @odata.nextLink — a full URL to the next page. Follow it until it's gone:

async function getAllMessages(token: string) {
  let url =
    "https://graph.microsoft.com/v1.0/me/messages?$select=subject&$top=50";
  const all: unknown[] = [];

  while (url) {
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${token}` },
    });
    const data = await res.json();
    all.push(...data.value);
    url = data["@odata.nextLink"]; // undefined on the last page → loop ends
  }
  return all;
}

Forgetting to follow nextLink is why people swear "Graph only returns 10 emails." It doesn't — you're seeing page one.

Writing data

Graph isn't read-only. Creating a calendar event is a POST with a JSON body:

await fetch("https://graph.microsoft.com/v1.0/me/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    subject: "Project kickoff",
    start: { dateTime: "2026-07-20T15:00:00", timeZone: "UTC" },
    end: { dateTime: "2026-07-20T15:30:00", timeZone: "UTC" },
    attendees: [
      {
        emailAddress: { address: "client@example.com", name: "Client" },
        type: "required",
      },
    ],
  }),
});

The same shape applies to sending mail (/me/sendMail), creating files, and adding contacts — a JSON body matching Graph's resource schema.

Calling Graph from an Office add-in (SSO)

Inside an Outlook or Office add-in, you don't want to force a separate login — the user is already signed into Office. Office SSO bridges that: Office.js hands you a token you exchange (server-side) for a Graph token.

const bootstrapToken = await Office.auth.getAccessToken({
  allowSignInPrompt: true,
});
// Send bootstrapToken to your backend, exchange it for a Graph token
// via the on-behalf-of flow, then call Graph from the server.

This is exactly how a professional Outlook add-in shows CRM or ticket data on the open email without a second login — a core part of my Microsoft 365 development work. If you're just starting with add-ins, begin with the Outlook add-in guide and layer Graph on top.

Calling Graph from a Node.js backend (app permissions)

For background jobs with no user, use the client credentials flow with MSAL Node. The app authenticates as itself using its client secret:

import { ConfidentialClientApplication } from "@azure/msal-node";

const cca = new ConfidentialClientApplication({
  auth: {
    clientId: process.env.CLIENT_ID!,
    authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
    clientSecret: process.env.CLIENT_SECRET!,
  },
});

async function getAppToken() {
  const result = await cca.acquireTokenByClientCredential({
    scopes: ["https://graph.microsoft.com/.default"],
  });
  return result?.accessToken;
}

The .default scope means "every application permission this app has been granted consent for." Grant those permissions once in the Entra portal, and this token can read across the tenant — perfect for reporting and sync jobs.

Debugging tips

  • Use Graph Explorer. aka.ms/ge lets you run authenticated calls in the browser — the fastest way to confirm a request works before you write code.
  • Read the error body. Graph returns a JSON error with a code and message that usually names the exact missing permission.
  • 403 almost always means scopes. Wrong permission type (delegated vs application) or missing admin consent — not a bug in your code.
  • 401 means the token is missing, expired, or for the wrong audience.

Where to go next

Once you're comfortable with tokens, scopes, and pagination, the rest of Graph is just more endpoints following the same rules. Build a small delegated app against /me first, get one clean request working end to end, then expand.

If you'd rather have Graph integration — SSO, the on-behalf-of flow, and least-privilege scopes — designed and built correctly the first time, that's the Microsoft 365 development work I do for clients around the world.

Related services

Need this built for you? Here's where I can help.

Zia ul Qamar

Written by

Zia ul Qamar

Full-stack developer specializing in React, Next.js, and Node.js, with a rare focus on Microsoft Office & Google Workspace add-ins. I build production tools and automations for international clients.