Microsoft 365 Integration for ChatGPT Apps: Complete Guide
Integrating ChatGPT apps with Microsoft 365 unlocks powerful productivity scenarios for enterprise users. From automated email responses in Outlook to intelligent document search in SharePoint, Microsoft Graph API enables deep integration with the world's most popular business productivity suite.
This comprehensive guide covers everything you need to build production-ready ChatGPT apps that seamlessly integrate with Outlook, Teams, SharePoint, OneDrive, and other Microsoft 365 services. You'll learn authentication patterns, API implementation, and best practices for enterprise deployment.
Table of Contents
- Microsoft Graph API Overview
- Azure AD OAuth 2.1 Authentication
- Outlook Integration
- Microsoft Teams Integration
- SharePoint & OneDrive Integration
- Delegated vs Application Permissions
- Production Best Practices
Microsoft Graph API Overview
Microsoft Graph API is the unified gateway to Microsoft 365 data and intelligence. It provides a single endpoint (https://graph.microsoft.com) to access user data across Outlook, Teams, SharePoint, OneDrive, Calendar, Contacts, and more.
Why Microsoft 365 Integration for ChatGPT Apps?
Enterprise Productivity: 345 million Microsoft 365 commercial users worldwide rely on Outlook, Teams, and SharePoint daily. ChatGPT apps that integrate with these tools can automate workflows, surface insights, and reduce context-switching.
Natural Language Interface: Users can interact with their Microsoft 365 data through conversational prompts instead of navigating complex UIs. "Show my unread emails from this week" becomes a simple ChatGPT query.
Contextual Intelligence: ChatGPT can analyze email threads, meeting transcripts, and SharePoint documents to provide intelligent summaries, action items, and recommendations.
Key Microsoft 365 Services for ChatGPT Apps
- Outlook: Email automation, calendar scheduling, contact management
- Microsoft Teams: Chat bots, message posting, channel management
- SharePoint: Document search, list management, site administration
- OneDrive: File storage, document collaboration, real-time sync
- Azure AD: Single sign-on, user management, security policies
For detailed authentication patterns across all enterprise platforms, see our Enterprise Authentication for ChatGPT Apps guide.
Azure AD OAuth 2.1 Authentication {#azure-ad-oauth-authentication}
Microsoft 365 integration requires OAuth 2.1 with PKCE (Proof Key for Code Exchange) for secure user authentication. This is the same pattern required by OpenAI Apps SDK.
Graph API Client Implementation (120 lines)
// lib/microsoft-graph-client.ts
import { Client } from "@microsoft/microsoft-graph-client";
import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials";
import { ClientSecretCredential, OnBehalfOfCredential } from "@azure/identity";
interface GraphClientConfig {
tenantId: string;
clientId: string;
clientSecret: string;
scopes?: string[];
}
interface UserTokenConfig {
tenantId: string;
clientId: string;
clientSecret: string;
userAccessToken: string;
scopes: string[];
}
export class MicrosoftGraphClient {
private client: Client;
private credential: ClientSecretCredential | OnBehalfOfCredential;
/**
* Initialize Graph client with application credentials (app-only access)
*/
static createAppClient(config: GraphClientConfig): MicrosoftGraphClient {
const credential = new ClientSecretCredential(
config.tenantId,
config.clientId,
config.clientSecret
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: config.scopes || ["https://graph.microsoft.com/.default"]
});
const client = Client.initWithMiddleware({ authProvider });
return new MicrosoftGraphClient(client, credential);
}
/**
* Initialize Graph client with user token (delegated access via OBO flow)
*/
static createUserClient(config: UserTokenConfig): MicrosoftGraphClient {
const credential = new OnBehalfOfCredential({
tenantId: config.tenantId,
clientId: config.clientId,
clientSecret: config.clientSecret,
userAssertionToken: config.userAccessToken
});
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: config.scopes
});
const client = Client.initWithMiddleware({ authProvider });
return new MicrosoftGraphClient(client, credential);
}
private constructor(client: Client, credential: ClientSecretCredential | OnBehalfOfCredential) {
this.client = client;
this.credential = credential;
}
/**
* Get authenticated Graph client instance
*/
getClient(): Client {
return this.client;
}
/**
* Execute paginated Graph API request with automatic retry
*/
async executePaginatedRequest<T>(
endpoint: string,
maxResults: number = 100
): Promise<T[]> {
const results: T[] = [];
let nextLink: string | undefined = endpoint;
while (nextLink && results.length < maxResults) {
try {
const response = await this.client
.api(nextLink)
.top(Math.min(100, maxResults - results.length))
.get();
results.push(...(response.value || []));
nextLink = response["@odata.nextLink"];
} catch (error) {
if (this.isRetryableError(error)) {
await this.delay(1000);
continue;
}
throw error;
}
}
return results.slice(0, maxResults);
}
/**
* Check if error is retryable (429 throttling, 503 service unavailable)
*/
private isRetryableError(error: any): boolean {
const statusCode = error?.statusCode || error?.response?.status;
return statusCode === 429 || statusCode === 503;
}
/**
* Delay helper for retry logic
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Batch multiple Graph API requests (up to 20 requests per batch)
*/
async executeBatch(requests: Array<{ id: string; url: string; method: string }>): Promise<any> {
const batchRequestContent = {
requests: requests.map(req => ({
id: req.id,
method: req.method,
url: req.url
}))
};
return await this.client.api("/$batch").post(batchRequestContent);
}
}
// Example usage:
// const graphClient = MicrosoftGraphClient.createAppClient({
// tenantId: process.env.AZURE_TENANT_ID!,
// clientId: process.env.AZURE_CLIENT_ID!,
// clientSecret: process.env.AZURE_CLIENT_SECRET!
// });
OAuth Protected Resource Metadata
Microsoft 365 integration requires publishing OAuth metadata at .well-known/oauth-protected-resource:
{
"resource": "https://api.yourdomain.com",
"authorization_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize",
"token_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
"token_endpoint_auth_methods_supported": ["client_secret_post"],
"grant_types_supported": ["authorization_code"],
"response_types_supported": ["code"],
"scopes_supported": [
"Mail.Read",
"Mail.Send",
"Calendars.ReadWrite",
"Files.ReadWrite.All",
"Sites.ReadWrite.All",
"User.Read"
]
}
For comprehensive OAuth implementation patterns, see our ChatGPT App Builder Authentication Guide.
Outlook Integration
Outlook integration enables ChatGPT apps to read emails, send messages, manage calendar events, and access contacts. This is one of the most requested enterprise features.
Outlook Email Handler (130 lines)
// mcp-tools/outlook-handler.ts
import { MicrosoftGraphClient } from "../lib/microsoft-graph-client";
import type { Message, SendMail } from "@microsoft/microsoft-graph-types";
interface OutlookToolConfig {
graphClient: MicrosoftGraphClient;
userId?: string; // Optional: specific user, or use "me" for authenticated user
}
interface EmailSearchParams {
folder?: string; // "inbox" | "sentitems" | "drafts"
unreadOnly?: boolean;
from?: string;
subject?: string;
since?: Date;
maxResults?: number;
}
export class OutlookHandler {
private client: MicrosoftGraphClient;
private userId: string;
constructor(config: OutlookToolConfig) {
this.client = config.graphClient;
this.userId = config.userId || "me";
}
/**
* Search emails with natural language filters
*/
async searchEmails(params: EmailSearchParams): Promise<Message[]> {
const filters: string[] = [];
if (params.unreadOnly) {
filters.push("isRead eq false");
}
if (params.from) {
filters.push(`from/emailAddress/address eq '${params.from}'`);
}
if (params.subject) {
filters.push(`contains(subject, '${params.subject}')`);
}
if (params.since) {
const isoDate = params.since.toISOString();
filters.push(`receivedDateTime ge ${isoDate}`);
}
const folder = params.folder || "inbox";
const filterQuery = filters.length > 0 ? `?$filter=${filters.join(" and ")}` : "";
const endpoint = `/users/${this.userId}/mailFolders/${folder}/messages${filterQuery}`;
return await this.client.executePaginatedRequest<Message>(
endpoint,
params.maxResults || 50
);
}
/**
* Get email by ID with full body content
*/
async getEmail(messageId: string): Promise<Message> {
const graphClient = this.client.getClient();
return await graphClient
.api(`/users/${this.userId}/messages/${messageId}`)
.select("subject,from,toRecipients,receivedDateTime,body,hasAttachments")
.get();
}
/**
* Send email with optional attachments
*/
async sendEmail(params: {
to: string[];
subject: string;
body: string;
cc?: string[];
bcc?: string[];
attachments?: Array<{ name: string; contentBytes: string }>;
}): Promise<void> {
const graphClient = this.client.getClient();
const message: SendMail = {
message: {
subject: params.subject,
body: {
contentType: "HTML",
content: params.body
},
toRecipients: params.to.map(email => ({
emailAddress: { address: email }
})),
ccRecipients: params.cc?.map(email => ({
emailAddress: { address: email }
})),
bccRecipients: params.bcc?.map(email => ({
emailAddress: { address: email }
})),
attachments: params.attachments?.map(att => ({
"@odata.type": "#microsoft.graph.fileAttachment",
name: att.name,
contentBytes: att.contentBytes
}))
},
saveToSentItems: true
};
await graphClient.api(`/users/${this.userId}/sendMail`).post(message);
}
/**
* Mark email as read/unread
*/
async updateReadStatus(messageId: string, isRead: boolean): Promise<void> {
const graphClient = this.client.getClient();
await graphClient
.api(`/users/${this.userId}/messages/${messageId}`)
.update({ isRead });
}
/**
* Get calendar events for date range
*/
async getCalendarEvents(params: {
startDate: Date;
endDate: Date;
maxResults?: number;
}): Promise<any[]> {
const startDateTime = params.startDate.toISOString();
const endDateTime = params.endDate.toISOString();
const endpoint = `/users/${this.userId}/calendar/calendarView?startDateTime=${startDateTime}&endDateTime=${endDateTime}`;
return await this.client.executePaginatedRequest(
endpoint,
params.maxResults || 100
);
}
/**
* Create calendar event
*/
async createCalendarEvent(params: {
subject: string;
startTime: Date;
endTime: Date;
attendees?: string[];
location?: string;
body?: string;
}): Promise<any> {
const graphClient = this.client.getClient();
const event = {
subject: params.subject,
start: {
dateTime: params.startTime.toISOString(),
timeZone: "UTC"
},
end: {
dateTime: params.endTime.toISOString(),
timeZone: "UTC"
},
attendees: params.attendees?.map(email => ({
emailAddress: { address: email },
type: "required"
})),
location: params.location ? { displayName: params.location } : undefined,
body: params.body ? {
contentType: "HTML",
content: params.body
} : undefined
};
return await graphClient.api(`/users/${this.userId}/calendar/events`).post(event);
}
}
MCP Tool Definition for Outlook
{
name: "search_outlook_emails",
description: "Search user's Outlook emails with filters (unread, from, subject, date range)",
inputSchema: {
type: "object",
properties: {
folder: { type: "string", enum: ["inbox", "sentitems", "drafts"], default: "inbox" },
unreadOnly: { type: "boolean", default: false },
from: { type: "string", description: "Filter by sender email address" },
subject: { type: "string", description: "Filter by subject keywords" },
sinceDays: { type: "number", description: "Show emails from last N days" },
maxResults: { type: "number", default: 20, maximum: 100 }
}
}
}
For calendar-specific integration patterns, see our Calendar Scheduling for ChatGPT Apps guide.
Microsoft Teams Integration
Microsoft Teams integration allows ChatGPT apps to post messages, create channels, and build interactive bots. Teams has 280 million monthly active users, making it a critical enterprise integration.
Teams Bot Handler (110 lines)
// mcp-tools/teams-handler.ts
import { MicrosoftGraphClient } from "../lib/microsoft-graph-client";
import type { Channel, ChatMessage, Team } from "@microsoft/microsoft-graph-types";
interface TeamsToolConfig {
graphClient: MicrosoftGraphClient;
}
export class TeamsHandler {
private client: MicrosoftGraphClient;
constructor(config: TeamsToolConfig) {
this.client = config.graphClient;
}
/**
* List all teams the user is a member of
*/
async listTeams(): Promise<Team[]> {
return await this.client.executePaginatedRequest<Team>("/me/joinedTeams", 50);
}
/**
* List channels in a team
*/
async listChannels(teamId: string): Promise<Channel[]> {
const graphClient = this.client.getClient();
const response = await graphClient.api(`/teams/${teamId}/channels`).get();
return response.value || [];
}
/**
* Post message to Teams channel
*/
async postChannelMessage(params: {
teamId: string;
channelId: string;
message: string;
mentions?: Array<{ userId: string; displayName: string }>;
}): Promise<ChatMessage> {
const graphClient = this.client.getClient();
const chatMessage: any = {
body: {
contentType: "html",
content: params.message
}
};
// Add @mentions if provided
if (params.mentions && params.mentions.length > 0) {
chatMessage.mentions = params.mentions.map((mention, index) => ({
id: index,
mentionText: mention.displayName,
mentioned: {
user: {
id: mention.userId,
displayName: mention.displayName
}
}
}));
}
return await graphClient
.api(`/teams/${params.teamId}/channels/${params.channelId}/messages`)
.post(chatMessage);
}
/**
* Get channel messages (conversation history)
*/
async getChannelMessages(params: {
teamId: string;
channelId: string;
maxResults?: number;
}): Promise<ChatMessage[]> {
return await this.client.executePaginatedRequest<ChatMessage>(
`/teams/${params.teamId}/channels/${params.channelId}/messages`,
params.maxResults || 50
);
}
/**
* Create new channel in team
*/
async createChannel(params: {
teamId: string;
displayName: string;
description?: string;
membershipType?: "standard" | "private";
}): Promise<Channel> {
const graphClient = this.client.getClient();
const channel = {
displayName: params.displayName,
description: params.description || "",
membershipType: params.membershipType || "standard"
};
return await graphClient.api(`/teams/${params.teamId}/channels`).post(channel);
}
/**
* Reply to existing message (threaded conversation)
*/
async replyToMessage(params: {
teamId: string;
channelId: string;
messageId: string;
reply: string;
}): Promise<ChatMessage> {
const graphClient = this.client.getClient();
const chatMessage = {
body: {
contentType: "html",
content: params.reply
}
};
return await graphClient
.api(`/teams/${params.teamId}/channels/${params.channelId}/messages/${params.messageId}/replies`)
.post(chatMessage);
}
/**
* Search messages across all teams (requires elevated permissions)
*/
async searchMessages(query: string, maxResults: number = 20): Promise<ChatMessage[]> {
const graphClient = this.client.getClient();
const searchRequest = {
requests: [
{
entityTypes: ["chatMessage"],
query: {
queryString: query
},
from: 0,
size: maxResults
}
]
};
const response = await graphClient.api("/search/query").post(searchRequest);
const hits = response.value?.[0]?.hitsContainers?.[0]?.hits || [];
return hits.map((hit: any) => hit.resource);
}
}
Required Teams Permissions
| Permission | Type | Description |
|---|---|---|
Team.ReadBasic.All |
Delegated | Read team names and descriptions |
Channel.ReadBasic.All |
Delegated | Read channel names |
ChannelMessage.Send |
Delegated | Post messages to channels |
ChannelMessage.Read.All |
Application | Read all channel messages (bot) |
TeamMember.Read.All |
Application | Read team memberships |
SharePoint & OneDrive Integration {#sharepoint-onedrive-integration}
SharePoint and OneDrive integration enables document management, search, and collaboration workflows within ChatGPT apps.
SharePoint Connector (100 lines)
// mcp-tools/sharepoint-handler.ts
import { MicrosoftGraphClient } from "../lib/microsoft-graph-client";
import type { Site, List, ListItem, DriveItem } from "@microsoft/microsoft-graph-types";
interface SharePointToolConfig {
graphClient: MicrosoftGraphClient;
}
export class SharePointHandler {
private client: MicrosoftGraphClient;
constructor(config: SharePointToolConfig) {
this.client = config.graphClient;
}
/**
* Search SharePoint sites
*/
async searchSites(query: string): Promise<Site[]> {
const graphClient = this.client.getClient();
const response = await graphClient.api(`/sites?search=${encodeURIComponent(query)}`).get();
return response.value || [];
}
/**
* Get site by URL
*/
async getSiteByUrl(siteUrl: string): Promise<Site> {
const graphClient = this.client.getClient();
// Format: /sites/{hostname}:/{server-relative-path}
const [hostname, ...pathParts] = siteUrl.replace("https://", "").split("/");
const path = pathParts.join("/");
return await graphClient.api(`/sites/${hostname}:/${path}`).get();
}
/**
* List all lists in a site
*/
async getLists(siteId: string): Promise<List[]> {
const graphClient = this.client.getClient();
const response = await graphClient.api(`/sites/${siteId}/lists`).get();
return response.value || [];
}
/**
* Get list items with optional filtering
*/
async getListItems(params: {
siteId: string;
listId: string;
filter?: string;
expand?: string[];
maxResults?: number;
}): Promise<ListItem[]> {
let endpoint = `/sites/${params.siteId}/lists/${params.listId}/items`;
const queryParams: string[] = [];
if (params.filter) {
queryParams.push(`$filter=${encodeURIComponent(params.filter)}`);
}
if (params.expand) {
queryParams.push(`$expand=${params.expand.join(",")}`);
}
if (queryParams.length > 0) {
endpoint += `?${queryParams.join("&")}`;
}
return await this.client.executePaginatedRequest<ListItem>(
endpoint,
params.maxResults || 100
);
}
/**
* Create list item
*/
async createListItem(params: {
siteId: string;
listId: string;
fields: Record<string, any>;
}): Promise<ListItem> {
const graphClient = this.client.getClient();
return await graphClient
.api(`/sites/${params.siteId}/lists/${params.listId}/items`)
.post({ fields: params.fields });
}
/**
* Update list item
*/
async updateListItem(params: {
siteId: string;
listId: string;
itemId: string;
fields: Record<string, any>;
}): Promise<ListItem> {
const graphClient = this.client.getClient();
return await graphClient
.api(`/sites/${params.siteId}/lists/${params.listId}/items/${params.itemId}`)
.update({ fields: params.fields });
}
/**
* Search documents across SharePoint
*/
async searchDocuments(query: string, maxResults: number = 20): Promise<DriveItem[]> {
const graphClient = this.client.getClient();
const searchRequest = {
requests: [
{
entityTypes: ["driveItem"],
query: {
queryString: query
},
from: 0,
size: maxResults
}
]
};
const response = await graphClient.api("/search/query").post(searchRequest);
const hits = response.value?.[0]?.hitsContainers?.[0]?.hits || [];
return hits.map((hit: any) => hit.resource);
}
}
OneDrive File Sync (80 lines)
// mcp-tools/onedrive-handler.ts
import { MicrosoftGraphClient } from "../lib/microsoft-graph-client";
import type { DriveItem, Drive } from "@microsoft/microsoft-graph-types";
interface OneDriveToolConfig {
graphClient: MicrosoftGraphClient;
userId?: string;
}
export class OneDriveHandler {
private client: MicrosoftGraphClient;
private userId: string;
constructor(config: OneDriveToolConfig) {
this.client = config.graphClient;
this.userId = config.userId || "me";
}
/**
* Get user's default drive
*/
async getDefaultDrive(): Promise<Drive> {
const graphClient = this.client.getClient();
return await graphClient.api(`/users/${this.userId}/drive`).get();
}
/**
* List files in folder
*/
async listFiles(folderId: string = "root", maxResults: number = 100): Promise<DriveItem[]> {
return await this.client.executePaginatedRequest<DriveItem>(
`/users/${this.userId}/drive/items/${folderId}/children`,
maxResults
);
}
/**
* Upload file to OneDrive
*/
async uploadFile(params: {
fileName: string;
content: Buffer | string;
folderId?: string;
conflictBehavior?: "replace" | "rename" | "fail";
}): Promise<DriveItem> {
const graphClient = this.client.getClient();
const folderId = params.folderId || "root";
return await graphClient
.api(`/users/${this.userId}/drive/items/${folderId}:/${params.fileName}:/content`)
.header("Content-Type", "application/octet-stream")
.put(params.content);
}
/**
* Download file from OneDrive
*/
async downloadFile(fileId: string): Promise<Buffer> {
const graphClient = this.client.getClient();
// Get download URL
const driveItem: DriveItem = await graphClient
.api(`/users/${this.userId}/drive/items/${fileId}`)
.get();
if (!driveItem["@microsoft.graph.downloadUrl"]) {
throw new Error("File has no download URL");
}
// Download file content
const response = await fetch(driveItem["@microsoft.graph.downloadUrl"]);
return Buffer.from(await response.arrayBuffer());
}
/**
* Create sharing link for file
*/
async createSharingLink(params: {
fileId: string;
type: "view" | "edit";
scope: "anonymous" | "organization";
}): Promise<string> {
const graphClient = this.client.getClient();
const permission = await graphClient
.api(`/users/${this.userId}/drive/items/${params.fileId}/createLink`)
.post({
type: params.type,
scope: params.scope
});
return permission.link.webUrl;
}
/**
* Search files across OneDrive
*/
async searchFiles(query: string, maxResults: number = 50): Promise<DriveItem[]> {
return await this.client.executePaginatedRequest<DriveItem>(
`/users/${this.userId}/drive/root/search(q='${encodeURIComponent(query)}')`,
maxResults
);
}
}
Delegated vs Application Permissions {#delegated-vs-application-permissions}
Understanding Microsoft Graph permission types is critical for ChatGPT app security and compliance.
Delegated Permissions (User Context)
Use When: ChatGPT app acts on behalf of a signed-in user. The app can only access data the user has permission to access.
OAuth Flow: Authorization code with PKCE (user consent required)
Common Delegated Permissions:
Mail.Read- Read user's mailMail.Send- Send mail as userCalendars.ReadWrite- Manage user's calendarFiles.ReadWrite- Access user's OneDrive filesUser.Read- Read user's profile
Example: A ChatGPT app that summarizes your unread emails requires Mail.Read delegated permission.
Application Permissions (App Context)
Use When: ChatGPT app needs to access data across all users without a signed-in user (background jobs, administrative tasks).
OAuth Flow: Client credentials (admin consent required)
Common Application Permissions:
Mail.Read.All- Read all mailboxesCalendars.Read.All- Read all calendarsFiles.Read.All- Read all files across organizationSites.ReadWrite.All- Manage all SharePoint sites
Example: A ChatGPT app that monitors all team emails for compliance requires Mail.Read.All application permission.
Permission Selection Best Practices
- Principle of Least Privilege: Request only the minimum permissions needed
- Delegated by Default: Use delegated permissions unless background access is required
- Admin Consent Planning: Application permissions require tenant admin approval
- Audit Trail: Log all Graph API calls for compliance and security auditing
For more authentication patterns, see our Enterprise Authentication guide.
Production Best Practices
Rate Limiting & Throttling
Microsoft Graph API enforces rate limits (varies by endpoint, typically 10,000 requests/10 minutes per app). Handle throttling gracefully:
async function graphRequestWithRetry<T>(
requestFn: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await requestFn();
} catch (error: any) {
if (error.statusCode === 429) {
const retryAfter = parseInt(error.headers?.["retry-after"] || "5");
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
throw error;
}
}
throw new Error(`Request failed after ${maxRetries} retries`);
}
Token Caching
Cache access tokens to minimize token endpoint calls (reduces latency by 200-500ms per request):
import NodeCache from "node-cache";
const tokenCache = new NodeCache({ stdTTL: 3000 }); // 50 min cache (tokens expire in 60 min)
async function getCachedToken(userId: string): Promise<string> {
const cached = tokenCache.get<string>(`token:${userId}`);
if (cached) return cached;
const token = await acquireToken(userId);
tokenCache.set(`token:${userId}`, token);
return token;
}
Error Handling
Graph API returns structured errors. Parse and handle them appropriately:
interface GraphError {
statusCode: number;
code: string;
message: string;
requestId: string;
date: string;
}
function handleGraphError(error: any): never {
const graphError: GraphError = error;
switch (graphError.code) {
case "InvalidAuthenticationToken":
throw new Error("Token expired. Please re-authenticate.");
case "AccessDenied":
throw new Error("Insufficient permissions. Contact your admin.");
case "ResourceNotFound":
throw new Error("Requested resource not found.");
case "ServiceNotAvailable":
throw new Error("Microsoft 365 service temporarily unavailable. Retry in 30s.");
default:
throw new Error(`Graph API error: ${graphError.message} (${graphError.code})`);
}
}
Batch Requests for Performance
Combine multiple Graph API calls into a single batch request (reduces latency from 5 sequential requests → 1 batch request):
const batchResponse = await graphClient.executeBatch([
{ id: "1", url: "/me/messages?$top=10", method: "GET" },
{ id: "2", url: "/me/calendar/events?$top=5", method: "GET" },
{ id: "3", url: "/me/drive/root/children", method: "GET" }
]);
const emails = batchResponse.responses.find(r => r.id === "1").body.value;
const events = batchResponse.responses.find(r => r.id === "2").body.value;
const files = batchResponse.responses.find(r => r.id === "3").body.value;
Security Considerations
- Never Log Access Tokens: Tokens grant full access to user data
- Validate Redirect URIs: Only allowlist production domains
- Use HTTPS Everywhere: Graph API requires TLS 1.2+
- Implement PKCE: Prevents authorization code interception attacks
- Rotate Client Secrets: Update secrets every 90 days
For email-specific integration patterns, see our Email Automation for ChatGPT Apps guide.
Building Your First Microsoft 365 ChatGPT App
Ready to build enterprise-grade ChatGPT apps with Microsoft 365 integration? MakeAIHQ provides a no-code platform specifically designed for ChatGPT app development with built-in Microsoft Graph API support.
What You Get with MakeAIHQ:
- Pre-built Microsoft 365 Templates: Outlook email assistant, Teams bot, SharePoint document search
- OAuth 2.1 Configuration Wizard: Generate Azure AD app registration with correct permissions
- Graph API Client Generation: Auto-generate TypeScript clients for Outlook, Teams, SharePoint, OneDrive
- Testing & Debugging Tools: Test Graph API calls with live user tokens
- Deployment in 48 Hours: From concept to production ChatGPT Store submission
Next Steps:
- Explore Microsoft 365 Templates - See pre-built integrations
- Start Free Trial - 1 free app, 24-hour trial access
- Read Enterprise Authentication Guide - Deep dive on OAuth patterns
- ChatGPT App Development Pillar Guide - Complete enterprise development guide
Microsoft 365 integration unlocks the full potential of ChatGPT apps for enterprise users. With 345 million commercial users worldwide, your ChatGPT app can become an indispensable productivity tool.
Build your Microsoft 365 ChatGPT app today with MakeAIHQ - no coding required.
Related Resources
- Enterprise Authentication for ChatGPT Apps - OAuth 2.1, SAML, SSO patterns
- Calendar Scheduling for ChatGPT Apps - Outlook Calendar integration deep dive
- Email Automation for ChatGPT Apps - Advanced Outlook email workflows
- Enterprise ChatGPT App Development (Pillar) - Complete enterprise guide
- Microsoft Graph API Documentation - Official Microsoft docs
- Azure AD App Registration Guide - Setup OAuth apps
Ready to integrate Microsoft 365 with ChatGPT? Start building your app today →