Outlook email webhooks let your application react when an Outlook mailbox changes without repeatedly polling for new messages. In practice, that means creating a Microsoft Graph subscription, receiving a small HTTP notification at your endpoint, and then safely fetching or processing the affected email.
The important distinction is that Outlook itself does not expose a separate, generic webhook product. For Microsoft 365 and Outlook mailbox data, the supported route is Microsoft Graph change notifications. That distinction affects the URLs you use, the OAuth permissions you request, the short subscription lifetime you must manage, and how you should design the receiver.
What Outlook email webhooks actually do
A webhook is an HTTP callback: instead of your application asking Microsoft Graph every minute whether a mailbox changed, Graph sends an HTTPS POST to an endpoint you control after a subscribed resource changes.
For email automation, the usual resource is a collection of messages:
/me/messages
/users/{user-id-or-user-principal-name}/messages
/users/{user-id-or-user-principal-name}/mailFolders/{folder-id}/messages
A subscription can request notifications for one or more supported change types:
created— a message is created in the monitored collection.updated— a message changes, such as when it is marked as read, categorized, moved, or otherwise updated.deleted— a message is deleted from the monitored collection.
A notification is an event signal, not automatically a complete, authoritative copy of the email. In the normal notification model, Microsoft Graph tells you which resource changed. Your worker then calls Graph to retrieve the message fields needed for your workflow.
That is a sound architecture for use cases such as:
- Routing inbound support or sales messages into a CRM.
- Classifying new messages with an AI workflow.
- Alerting a team when an important sender emails.
- Synchronizing email metadata to a product database.
- Triggering document extraction when a mailbox receives invoices.
- Monitoring a dedicated shared mailbox with application permissions.
It is not a substitute for SMTP delivery, an email-sending API, or a mailbox backup system. It also should not be treated as a guarantee that every downstream action runs exactly once. Webhook delivery is an event-driven integration pattern, so production systems need idempotency, retries, renewal, and a reconciliation plan.
The Microsoft Graph webhook model
An Outlook webhook integration has four moving pieces:
- An app registration in Microsoft Entra ID. This gives your software an application (client) identity and defines the delegated or application permissions it can request.
- A Microsoft Graph access token. Your backend uses it to create subscriptions and read the messages referenced by notifications.
- A public HTTPS notification endpoint. Microsoft Graph calls this endpoint first to validate it and later to deliver changes.
- A subscription record. Created through
POST /subscriptions, it binds a Graph resource, the change types, a callback URL, an expiration time, and optional settings such asclientState.
The data flow looks like this:
Mailbox changes
-> Microsoft Graph detects a supported change
-> Graph POSTs a notification to your HTTPS endpoint
-> Your endpoint validates, stores, or queues the notification
-> A worker retrieves the message from Graph when needed
-> Your application performs its business action
Keep the HTTP receiver deliberately small. Its job is to validate the call, persist enough information, enqueue work, and return a successful response quickly. Downloading attachments, calling an LLM, writing to several SaaS APIs, or performing slow database work directly in the webhook request creates avoidable delivery failures.
Choose the right mailbox scope and permission model
The resource path and permission type determine which mailbox data your integration can watch. Design this before writing webhook code, because changing it later may require a new consent flow or tenant-admin approval.
Delegated permissions: a signed-in user's mailbox
Delegated access is appropriate when a user connects their own Outlook account to your application. The application obtains a token on behalf of that user and typically subscribes to a path beginning with /me.
For message notifications, Mail.Read is the least-privileged read permission commonly required. If your product needs only the signed-in user's mailbox, delegated Mail.Read has a smaller blast radius than tenant-wide application access.
A typical delegated resource is:
/me/messages
Use this approach for a personal inbox assistant, a founder's inbox triage tool, or a user-facing workflow where each person authorizes their own mailbox.
Application permissions: tenant-managed or shared mailboxes
Application permissions are appropriate for background services that must watch mailboxes without an interactive user session, such as an operations inbox or a shared support mailbox. The app uses the OAuth client-credentials flow and targets a specific user mailbox:
/users/support@contoso.com/mailFolders/inbox/messages
Application Mail.Read is powerful: it can permit access to mailbox data in the tenant after administrator consent. Treat it as sensitive production access. Use the smallest set of permissions, store credentials in a secrets manager, and apply Exchange application access controls where your organization uses them to limit which mailboxes the app can reach.
Do not assume a delegated token can act as a universal shortcut for shared and other users' mailboxes. Microsoft Graph's supported scope and permission rules vary by resource scenario. Test the exact subscription resource and consent model you intend to deploy.
Subscribe to all messages or a folder?
Subscribing to /users/{id}/messages is convenient, but it can generate updates for changes that do not matter to your workflow. A folder-specific subscription can reduce noise when the mailbox is organized intentionally.
For example, a dedicated automation can watch Inbox only:
/users/support@contoso.com/mailFolders/inbox/messages
If a rule moves messages into an Invoices folder, subscribe to that folder instead. Remember that moving a message changes its relationship to collections. Your processor should not infer too much from a single notification; retrieve the message or use a reconciliation process when folder membership matters.
Build an HTTPS endpoint Graph can validate
Before Graph will create a subscription, it validates the notificationUrl. The URL must be publicly reachable over HTTPS. A private development server on localhost cannot receive Graph callbacks directly; use a secure tunnel during local development or deploy a test endpoint.
When you create or renew a subscription, Microsoft Graph sends a POST to your notification URL containing a validationToken query parameter. Your endpoint must URL-decode that value and return it as the raw response body, with HTTP 200 OK and Content-Type: text/plain, within 10 seconds.
This is the validation behavior your route needs:
POST /webhooks/microsoft-graph?validationToken=abc%2B123
HTTP/1.1 200 OK
Content-Type: text/plain
abc+123
Do not wrap the token in JSON. Do not return { "validationToken": "abc+123" }. Do not require a session cookie or a login redirect. Any of those common patterns prevents Microsoft Graph from validating the endpoint.
Here is a minimal Node.js Express example:
import express from "express";
const app = express();
app.use(express.json({ type: ["application/json", "text/plain"] }));
app.post("/webhooks/microsoft-graph", async (req, res) => {
const validationToken = req.query.validationToken;
if (typeof validationToken === "string") {
return res
.status(200)
.type("text/plain")
.send(validationToken);
}
const notifications = req.body?.value;
if (!Array.isArray(notifications)) {
return res.sendStatus(400);
}
for (const notification of notifications) {
if (notification.clientState !== process.env.GRAPH_CLIENT_STATE) {
return res.sendStatus(403);
}
await enqueueNotification(notification);
}
return res.sendStatus(202);
});
The sample is intentionally simple. In a production service, enqueueNotification should write to a durable queue or database quickly, and the HTTP handler should not wait for expensive work to finish.
Development checklist
Before creating the real subscription, verify all of the following:
- The callback URL uses a publicly trusted HTTPS certificate.
- A direct
POSTto the route reaches your application. - The route returns a raw decoded
validationTokenastext/plain. - Your reverse proxy preserves the query string.
- Your platform does not redirect the webhook route from one host or path to another.
- Logs capture validation attempts and notification status codes without recording email contents or secrets.
A tunnel such as ngrok or Cloudflare Tunnel can help during development, but use a stable production domain for a long-running integration. Replacing a callback host means creating or renewing subscriptions against the new endpoint and validating it again.
Create an Outlook email webhook subscription
Create a subscription by sending an authenticated request to Microsoft Graph:
POST https://graph.microsoft.com/v1.0/subscriptions
Authorization: Bearer {access-token}
Content-Type: application/json
A practical request body for new messages in a dedicated Inbox looks like this:
{
"changeType": "created",
"notificationUrl": "https://api.example.com/webhooks/microsoft-graph",
"resource": "/users/support@contoso.com/mailFolders/inbox/messages",
"expirationDateTime": "2028-06-15T12:00:00Z",
"clientState": "replace-with-a-long-random-secret"
}
The timestamp above demonstrates the required ISO 8601 format only. It is not a valid evergreen value: Graph limits subscription lifetime by resource and notification type. Your application must calculate an expiration that is within the limit applicable to the resource, then renew before it expires.
For ordinary Outlook message notifications, Microsoft Graph documents a maximum lifetime of less than seven days. Rich notifications that include resource data have a shorter limit for Outlook resources. Do not hard-code a renewal interval based only on this article; read the subscription response, persist its expirationDateTime, and schedule renewals based on that value and the current Microsoft Graph limits.
A shell-oriented example can calculate a conservative normal-notification expiry six days ahead:
EXPIRY=$(node -e 'console.log(new Date(Date.now() + 6*24*60*60*1000).toISOString())')
curl --request POST \
--url https://graph.microsoft.com/v1.0/subscriptions \
--header "Authorization: Bearer $GRAPH_TOKEN" \
--header "Content-Type: application/json" \
--data "{
\"changeType\": \"created\",
\"notificationUrl\": \"https://api.example.com/webhooks/microsoft-graph\",
\"resource\": \"/users/support@contoso.com/mailFolders/inbox/messages\",
\"expirationDateTime\": \"$EXPIRY\",
\"clientState\": \"$GRAPH_CLIENT_STATE\"
}"
If validation succeeds, Graph returns a subscription object containing an id, the monitored resource, and its actual expirationDateTime. Store all three. The subscription ID is what you use for renewal and deletion.
Subscription creation failures that matter
A 400 response is often a malformed resource, an unsupported combination of fields, an invalid expiration date, or an issue with rich-notification encryption settings. A permission-related failure usually means the token lacks the required delegated or application permission, or the tenant administrator has not granted consent.
An endpoint-validation failure is different: Graph could not successfully call your notificationUrl or did not receive the exact token response in time. Check application logs, proxy logs, TLS configuration, redirects, and whether a web application firewall blocks Graph's request.
Process the notification safely
A standard change notification has a body shaped broadly like this:
{
"value": [
{
"subscriptionId": "11111111-2222-3333-4444-555555555555",
"changeType": "created",
"clientState": "replace-with-a-long-random-secret",
"resource": "Users/support@contoso.com/MailFolders('inbox')/Messages('AAMk...')",
"resourceData": {
"@odata.type": "#Microsoft.Graph.Message",
"@odata.id": "Users/support@contoso.com/MailFolders('inbox')/Messages('AAMk...')",
"id": "AAMk..."
}
}
]
}
Treat this as an instruction to process an item, not as a reliable full email record. Your worker can retrieve the message by ID using Graph, selecting only the properties it needs:
GET https://graph.microsoft.com/v1.0/users/support@contoso.com/messages/{message-id}?$select=id,subject,from,receivedDateTime,bodyPreview,hasAttachments,internetMessageId
Authorization: Bearer {access-token}
For an inbound routing workflow, metadata such as subject, from, receivedDateTime, and bodyPreview may be enough. Fetch the full body only when the workflow genuinely needs it, and handle attachments through their dedicated Graph endpoints rather than assuming they are embedded in the message response.
Use an idempotency key
Webhook systems can retry deliveries, batch notifications, and surface changes that lead your business logic to encounter the same message more than once. Your processor should be safe to run repeatedly.
A practical idempotency record can use:
subscriptionId + message id + changeType
For workflows centered on email identity across moves or duplicates, internetMessageId can also be useful after fetching the message. It is not a replacement for your own database constraints, but it can help correlate the same internet email in an application-level workflow.
Store an event status such as received, processing, completed, and failed. Make downstream writes use unique constraints or upserts. If your CRM accepts an external ID, use a stable message-related key so a retry updates the existing record rather than creating a duplicate lead or ticket.
Respond fast, then process asynchronously
Microsoft Graph expects a response quickly. Microsoft recommends returning 200 OK when your endpoint completes processing within about three seconds. If work will take longer, queue the notification and return 202 Accepted within the allowed response window.
A robust pipeline is:
- Receive the notification.
- Verify
clientStateand basic payload shape. - Write the event to durable storage or a queue.
- Return
202 Accepted. - Let a worker fetch the message, deduplicate, process it, and record the result.
- Retry worker failures with backoff and an alerting path.
This design prevents a slow AI model call, a temporarily unavailable CRM, or a large attachment from causing the webhook receiver itself to time out.
Standard notifications versus rich notifications
Microsoft Graph supports two broad notification patterns for eligible resources.
Standard notifications
Standard notifications carry change information and an identifier for the resource. Your application makes a follow-up Graph request to retrieve the message.
Choose this pattern when:
- You want the simplest webhook implementation.
- You already need a Graph fetch for attachments or extra fields.
- You want a clearer separation between event receipt and mailbox-data access.
- You want longer subscription lifetimes than the rich-notification option permits for Outlook resources.
For most inbox automation, standard notifications plus a targeted GET request are the best default.
Rich notifications with resource data
Rich notifications can include resource data in the notification payload, reducing the need for an immediate follow-up fetch. To use them, the subscription sets includeResourceData to true and supplies an encryption certificate and certificate ID. Graph encrypts the resource data; your service must decrypt it with the matching private key.
That extra configuration adds operational complexity:
- You must generate, protect, rotate, and make available a certificate private key.
- The receiver must implement decryption correctly.
- Outlook rich subscriptions have a shorter maximum lifetime than ordinary Outlook subscriptions.
- You still need durable processing, deduplication, renewal, and security controls.
Rich notifications are worthwhile when avoiding the extra Graph read is materially valuable and your team is comfortable operating certificate-based encryption. They are not automatically better for a small integration. Start with standard notifications unless you can name the specific latency, quota, or architecture constraint that rich notifications solve.
Renew subscriptions before they go dark
Subscriptions expire. Microsoft Graph does not turn a short-lived Outlook subscription into a permanent webhook registration. If you do not renew it, notifications stop at expirationDateTime.
Renew an existing subscription with PATCH /subscriptions/{subscription-id} and a new permitted expiration timestamp:
PATCH https://graph.microsoft.com/v1.0/subscriptions/11111111-2222-3333-4444-555555555555
Authorization: Bearer {access-token}
Content-Type: application/json
{
"expirationDateTime": "2028-06-21T12:00:00Z"
}
The date is illustrative; compute a valid date at runtime. Renewal can trigger endpoint validation, so your validation handler must remain deployed and correct for the life of the integration.
A renewal design that survives failures
Persist subscription records in your database with at least these fields:
- Internal tenant or connected-account ID.
- Microsoft Graph subscription ID.
- Resource path and change types.
- Callback endpoint version or URL identifier.
- Actual
expirationDateTimereturned by Graph. - Subscription status, last successful renewal, and last error.
Run a scheduled job frequently enough to renew well before expiry. It should query subscriptions approaching the renewal threshold, acquire a lock so two workers do not renew the same record simultaneously, call Graph, then update the stored expiration only after success.
Do not wait until the final minutes. A token acquisition issue, an endpoint-validation problem, an outage, or a deployment mistake can otherwise create an event gap. Alert when a subscription cannot be renewed and when its remaining lifetime falls below your operational safety margin.
Microsoft Graph also supports lifecycle notifications for supported subscriptions. Where available for your scenario, configure a separate lifecycleNotificationUrl and handle reauthorization or missed-notification signals as prompts to repair or reconcile the integration. Lifecycle notifications improve resilience; they do not eliminate the need for ordinary expiry monitoring.
Secure Outlook email webhooks and the data behind them
A webhook receiver is an internet-facing endpoint connected to sensitive mailbox data. Secure both the endpoint and the processing pipeline.
Validate clientState on every notification
Set clientState to a high-entropy secret when creating the subscription. Graph includes it in subsequent notifications. Compare it to the value stored for that subscription before accepting the event.
Use a constant-time comparison in security-sensitive implementations. Keep the value in secret storage rather than source control, and rotate it by replacing the subscription if exposure is suspected.
clientState is an application-level validation value, not a reason to skip other controls. Continue to use HTTPS, strict request parsing, rate limits where appropriate, least-privilege permissions, and private queue/database access.
Protect credentials and mail content
Never put a Graph bearer token, client secret, certificate private key, or raw email body in application logs. Redact Authorization headers and restrict observability tools that could capture request bodies.
For application permissions, use a managed identity or certificate-based client authentication where your hosting environment supports it rather than relying indefinitely on a long-lived client secret. Restrict access to the subscription-management service and audit who can change callback URLs or mailbox scope.
If you send extracted or classified data through another system, apply the same data-minimization rule: send only the fields that workflow needs. A classifier may need subject and sender, not full message bodies and attachments.
Handle hostile email safely
Email content is untrusted input. An email can contain malicious links, HTML, attachments, prompt-injection text, or instructions aimed at an automated AI workflow.
If you use an LLM after receiving a message, treat the email body as data, not as trusted system instructions. Keep action permissions narrow, require deterministic validation before making irreversible changes, scan attachments according to your security policy, and isolate file parsing. Do not allow an email alone to authorize a financial transfer, credential change, or broad administrative action.
A worked example: create a support-inbox automation
Suppose a SaaS company wants every new email arriving in support@contoso.com to create or update a support ticket. The mailbox is company-managed, so the backend uses application permissions rather than an individual employee's delegated login.
Step 1: Register the app and grant minimum access
Create an Entra ID app registration. Add the Microsoft Graph application permission required for reading mail, such as Mail.Read, and have a tenant administrator grant consent. Configure a certificate or other appropriate client credential for a server-side token flow.
Use a dedicated automation mailbox rather than a broad executive or general-purpose mailbox. This makes scope, retention, auditing, and testing easier.
Step 2: Deploy the callback endpoint
Deploy:
https://api.contoso.com/webhooks/microsoft-graph
Implement the validation-token behavior and normal notification handler described earlier. Store notifications in a queue named graph-mail-events and return 202 after the queue write succeeds.
Step 3: Create the subscription
Use this resource:
/users/support@contoso.com/mailFolders/inbox/messages
Use changeType: "created" initially. This avoids creating tickets again every time an agent marks a message as read or applies a category. Add updated only if your product has a defined reason to react to updates.
Generate a random clientState, calculate a valid expiration within the documented Outlook limit, and create the subscription. Save the returned subscription ID and expiration time.
Step 4: Receive and queue an event
When a customer emails the inbox, the receiver checks that clientState matches, then stores the subscription ID, message ID, resource string, and receipt timestamp. It returns 202 Accepted.
The queue record might look like:
{
"eventId": "internal-uuid",
"subscriptionId": "11111111-2222-3333-4444-555555555555",
"messageId": "AAMkAG...",
"changeType": "created",
"receivedAt": "2028-06-15T12:01:03Z"
}
Step 5: Fetch, deduplicate, and create the ticket
The worker calls Graph for the message. It checks whether a ticket already exists for the mailbox plus message ID or another stable correlation key. If not, it creates a ticket with the sender, subject, body preview, and message ID, then marks the event complete.
If Graph returns a transient error, the worker retries. If the message no longer exists because it was deleted or moved before processing, record the outcome and decide whether the correct business behavior is to skip it, search for it, or reconcile through a delta query.
Step 6: Prove that it works
A successful implementation has evidence at each layer:
- Subscription creation returns an ID and expiration date.
- Your endpoint logs the validation request and returns the raw token successfully.
- A test email produces a Graph notification with the expected subscription ID.
- The queue receives exactly one logical event, even if delivery retries occur.
- The worker fetches the intended message and creates one ticket.
- The renewal job extends the same subscription before expiry.
- Monitoring alerts if renewal or delivery processing fails.
That evidence is better than simply seeing a 200 once. Webhooks are only operationally successful when the endpoint, the worker, the mailbox read, deduplication, and renewal loop all work together.
Troubleshooting Outlook email webhooks
Most failed integrations fall into a small number of patterns.
“The subscription request fails validation”
Check that the callback is public HTTPS and that the exact route returns the decoded validationToken as plain text. Inspect whether a CDN, authentication middleware, redirect, or application firewall changes the request or response. Also test renewal validation, not only initial creation.
“The subscription was created, but no events arrive”
First inspect its expiration date and confirm it has not lapsed. Then verify that the monitored resource is the mailbox or folder where your test message actually arrives, that the requested changeType matches the event, and that your endpoint is still reachable.
For example, a subscription to Inbox will not necessarily help if a server rule immediately moves mail to another folder before your workflow assumptions run. Test with a plainly new message sent to the exact monitored folder.
“Events arrive, but Graph cannot retrieve the message”
Use the same mailbox identity and appropriate permission model for the follow-up read. A message can also be moved or deleted between notification and fetch. Design the worker to handle a missing item without repeatedly failing forever.
“My workflow creates duplicate tickets”
Assume duplicate processing is possible and make the operation idempotent. Record a unique key before calling the ticketing system, use database uniqueness constraints, and make retries resume the same logical job rather than creating a new one.
“Notifications stopped after working for several days”
This is usually a subscription-lifetime problem. Inspect the stored expirationDateTime, renewal logs, token acquisition, and any lifecycle notifications. Build alerts around expiry rather than waiting for an end user to notice missing automations.
When polling or delta query is the better companion
Webhooks reduce unnecessary polling and make automations more responsive, but they should not be your only change-tracking mechanism when completeness matters.
Microsoft Graph delta query provides a way to synchronize changes incrementally and obtain a continuation link for later syncs. A resilient design can use webhooks as the fast trigger and delta query as the reconciliation mechanism. For example, a webhook can wake a worker immediately, while a scheduled or recovery job runs a delta sync after an outage, a renewal failure, or a queue incident.
This hybrid approach is especially useful for systems that maintain a local index of messages. The webhook says, in effect, “something changed now”; the delta process helps establish “what is the current state since my last known checkpoint?”
Use direct polling only when webhooks are impossible in your environment, such as when you cannot expose a public HTTPS endpoint or cannot operate subscription renewal. Polling is simpler to prototype but adds latency and API traffic, and it still needs state management and error recovery.
Conclusion
The reliable way to build Outlook email webhooks is to use Microsoft Graph change notifications, not an Outlook-specific callback API. Create a least-privileged subscription for the correct mailbox resource, implement the plain-text validation handshake, verify clientState, queue events quickly, retrieve messages in a worker, deduplicate actions, and renew subscriptions before expiration.
The webhook itself is only the trigger. The production-grade integration is the system around it: secure OAuth access, durable queues, idempotent workers, monitoring, renewal automation, and a delta-query recovery path. Build those pieces from the beginning and Outlook mailbox automation becomes predictable rather than fragile.
FAQ
Can Outlook send a webhook when a new email arrives?
Yes. Microsoft Graph can send a change notification to your HTTPS endpoint when a subscribed Outlook message collection receives a created event. You create the subscription through the Microsoft Graph /subscriptions API.
Do Outlook email webhooks include the full email body?
Standard notifications generally identify the changed resource, after which your app fetches the message through Microsoft Graph. Rich notifications can include encrypted resource data for supported scenarios, but they require certificate configuration and have shorter subscription lifetimes for Outlook resources.
How long do Outlook webhook subscriptions last?
They expire and must be renewed. Microsoft Graph documents Outlook subscription limits by resource and notification type; standard Outlook message subscriptions are limited to less than seven days, while rich notifications have a shorter maximum. Persist the returned expiration date and renew before it.
Why does Microsoft Graph send a validation token to my webhook?
It verifies that you control the callback URL. Your endpoint must return the URL-decoded validationToken as a plain-text 200 OK response within 10 seconds when Graph creates or renews a subscription.
Are Outlook email webhooks secure enough for customer email?
They can be, but security depends on your implementation. Use HTTPS, least-privilege Graph permissions, a secret clientState check, protected OAuth credentials, durable processing, log redaction, and strict handling of untrusted email bodies and attachments.