Guide
Microsoft Graph Email API Quick Start
Microsoft Graph is the REST API for reading and sending Outlook and Microsoft 365 email. This guide walks through Azure AD app registration, OAuth 2.0 scope selection, delegated vs app-only authentication, sending and reading messages, common error codes, and when to skip Graph API setup entirely.
Written by Caleb Geene Director, Site Reliability Engineering
Command references used in this guide: nylas email send, nylas email list, nylas auth login, and nylas email read.
What is the Microsoft Graph email API?
Microsoft Graph is the unified REST API for Microsoft 365 services. The email endpoints live under /v1.0/me/messages (delegated) or /v1.0/users/{userId}/messages (app-only). Graph replaced the Exchange Web Services (EWS) SOAP API, which Microsoft begins blocking on October 1, 2026, with permanent shutdown on April 1, 2027. Graph is the primary API for all Microsoft 365 services including Outlook, Teams, OneDrive, and SharePoint.
The API supports sending, reading, listing, searching, and managing email folders, attachments, and rules. Authentication goes through Azure AD (now Microsoft Entra ID) using OAuth 2.0. Two auth modes exist: delegated (user signs in via browser) and app-only (client credentials, no user present). The choice between them determines which scopes you request and how the API identifies the mailbox.
How do you register an Azure AD app for email?
Every Graph API call requires an Azure AD app registration. This creates a client ID and client secret that your code exchanges for an access token. The registration process takes about 10 minutes in the Azure portal. You need a Microsoft 365 tenant (a free developer tenant from the Microsoft 365 Developer Program works). The steps below are the minimum to get email working.
Navigate to Azure Portal > App registrations > New registration. Set the redirect URI to http://localhost:3000/callback for development. After registration, note the Application (client) ID and Directory (tenant) ID. Then go to Certificates & secrets to create a client secret. The secret expires after 6, 12, or 24 months, so you'll need to rotate it.
# After registering the app, you'll have these values:
CLIENT_ID="your-application-client-id"
TENANT_ID="your-directory-tenant-id"
CLIENT_SECRET="your-client-secret-value"
# Token endpoint for your tenant
TOKEN_URL="https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token"
# Request a delegated access token (authorization code flow)
curl -X POST "$TOKEN_URL" \
-d "client_id=$CLIENT_ID" \
-d "scope=https://graph.microsoft.com/Mail.Send https://graph.microsoft.com/Mail.Read" \
-d "client_secret=$CLIENT_SECRET" \
-d "grant_type=authorization_code" \
-d "redirect_uri=http://localhost:3000/callback" \
-d "code=<authorization-code-from-redirect>"The authorization code comes from the browser redirect after the user consents. For app-only (daemon) scenarios, replace grant_type=authorization_code with grant_type=client_credentials and use scope=https://graph.microsoft.com/.default. App-only tokens require admin consent, so a tenant admin must approve the permissions before the app can access any mailbox.
What OAuth scopes does Graph email require?
Microsoft Graph uses granular OAuth 2.0 scopes to control what an app can do. Email operations need specific scopes, and requesting the wrong ones causes 403 Forbidden errors. The table below lists every email-related scope, split by delegated (user present) and application (no user present) modes. According to Microsoft's permissions reference, 9 Mail.* permission scopes exist.
| Operation | Delegated scope | Application scope |
|---|---|---|
| Send email | Mail.Send | Mail.Send (admin consent) |
| Read email | Mail.Read | Mail.Read (admin consent) |
| Read + write email | Mail.ReadWrite | Mail.ReadWrite (admin consent) |
| Send as shared mailbox | Mail.Send.Shared | Not available |
| Read shared mailbox | Mail.Read.Shared | Not available |
Always request the minimum scopes your app needs. An app that only sends email should request Mail.Send alone, not Mail.ReadWrite. Application permissions (app-only) give access to every mailbox in the tenant unless the admin configures application access policies to restrict it to specific mailboxes. This is a security decision that requires tenant admin involvement.
How do you send email with Microsoft Graph?
Sending email through Graph is a POST request to /v1.0/me/sendMail (delegated) or /v1.0/users/{userId}/sendMail (app-only). The request body is a JSON object with a message field containing recipients, subject, and body. Unlike SMTP, you don't construct raw RFC 2822 messages or handle MIME encoding. Graph accepts HTML and plain text natively.
The example below sends a simple email using curl with a delegated access token. The saveToSentItems field controls whether the message appears in the sender's Sent Items folder. The 202 Accepted response means Graph queued the message for delivery. Actual delivery depends on Exchange Online transport rules and SPF/DKIM validation.
# Send email via Microsoft Graph
curl -X POST "https://graph.microsoft.com/v1.0/me/sendMail" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"subject": "Quarterly report",
"body": {
"contentType": "HTML",
"content": "<p>See the attached spreadsheet.</p>"
},
"toRecipients": [
{
"emailAddress": {
"address": "recipient@example.com"
}
}
]
},
"saveToSentItems": true
}'
# Expected response: HTTP 202 Accepted (empty body)For attachments, add a attachments array inside the message object. Each attachment needs a base64-encoded contentBytes field, a name, and contentType. Messages with attachments over 3 MB require a different flow: create a draft first, then use the /attachments/createUploadSession endpoint to upload the file in chunks of 4 MB each.
How do you read email with Microsoft Graph?
Reading email is a GET request to /v1.0/me/messages. Graph returns a paginated JSON array of message objects, defaulting to 10 messages per page. The $top query parameter controls page size (max 1,000). The $select parameter limits which fields come back, which reduces response size by up to 80% when you only need subject, from, and receivedDateTime.
The example below fetches the 5 most recent messages and filters to the subject, sender, and date fields. The $orderby parameter sorts newest-first. Pagination uses @odata.nextLink, which is a URL you call to get the next page. Each page includes a @odata.nextLink until you reach the last page.
# List recent emails with field selection
curl -s "https://graph.microsoft.com/v1.0/me/messages?\
$top=5&\
$select=subject,from,receivedDateTime&\
$orderby=receivedDateTime desc" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[]'
# Search for messages by subject
curl -s "https://graph.microsoft.com/v1.0/me/messages?\
$search=%22quarterly%20report%22&\
$top=10" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[].subject'
# Filter unread messages
curl -s "https://graph.microsoft.com/v1.0/me/messages?\
$filter=isRead eq false&\
$top=20" \
-H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value | length'Graph's $search uses Microsoft's search index, which supports KQL (Keyword Query Language) syntax. You can search by from:, subject:, body:, and other fields. The search index is eventually consistent, so newly arrived messages may take a few seconds to appear in search results. For real-time message monitoring, use change notifications (webhooks) instead of polling.
What are common Graph API errors and how do you fix them?
Graph returns standard HTTP status codes with a JSON error body. The 401 and 403 errors account for the majority of developer issues because they stem from token expiration, missing scopes, or admin consent gaps. The table below covers the 6 errors that come up most when working with email endpoints, based on the Graph error documentation.
| HTTP code | Error code | Cause and fix |
|---|---|---|
| 401 | InvalidAuthenticationToken | Token expired (tokens last 3,600 seconds). Refresh the token and retry. |
| 403 | Authorization_RequestDenied | Missing scope. Add Mail.Send or Mail.Read in Azure AD and re-consent. |
| 403 | MailboxNotEnabledForRESTAPI | Mailbox is on-premises or doesn't have an Exchange Online license. |
| 429 | TooManyRequests | Rate limited. Read the Retry-After header (in seconds) and wait. |
| 400 | ErrorInvalidRecipients | Malformed toRecipients array. Verify the JSON structure matches the schema. |
| 413 | MaximumValueExceeded | Attachment over 3 MB inline. Use the upload session endpoint instead. |
For 429 errors, Graph throttles per app per mailbox at 10,000 requests per 10 minutes for mail endpoints. The Retry-After header tells you exactly how many seconds to wait. Implement exponential backoff with jitter in production code. A tight polling loop checking every second can exhaust the quota in under 10 minutes.
When should you skip Graph API setup entirely?
Graph API setup requires an Azure AD app registration, client secret management, OAuth token refresh logic, and tenant admin consent for app-only permissions. That's appropriate for production apps with thousands of users. For developer scripts, CI/CD pipelines, one-off sends, and prototyping, the overhead isn't worth it.
The CLI authenticates Outlook accounts via OAuth in a browser, caches the token, and refreshes it automatically. You don't register anything in Azure AD, don't manage client secrets, and don't write token refresh code. These commands (`send`, `list`, `read`) work for Gmail, Yahoo, iCloud, Exchange, and IMAP, so you don't need a separate SDK per provider.
# Install (macOS or Linux)
brew install nylas/nylas-cli/nylas
# Authenticate your Outlook account
nylas auth login
# Send email (same command for any provider)
nylas email send \
--to recipient@example.com \
--subject "Quarterly report" \
--body "See the attached spreadsheet."
# List recent emails as JSON
nylas email list --limit 10 --json
# Read a specific message
nylas email read MESSAGE_ID --jsonFor applications that need Graph API directly, keep the Azure AD registration and use the error-fix table above. For developer tooling, scripts, and quick integrations, the CLI removes 15+ minutes of setup and eliminates the 401/403 errors that come from token and scope misconfiguration.
Next steps
- EWS to Graph migration — migrate from Exchange Web Services before the October 2026 shutdown
- Send-MgUserMail in PowerShell — full Graph PowerShell walkthrough with delegated and app-only auth
- List Outlook emails from terminal — read Outlook email without Graph API setup
- Outlook SMTP settings — smtp.office365.com reference with Modern Auth and error codes
- Full command reference — every flag and subcommand documented