Microsoft email webhooks are implemented through Microsoft Graph change notifications: your app creates a subscription for an Outlook mailbox or folder, and Microsoft Graph POSTs a notification when a message is created, updated, or deleted. The hard part is not creating the first subscription—it is validating the endpoint, handling short response deadlines, renewing subscriptions, and recovering safely when an event is missed.

What “Microsoft email webhooks” means

Microsoft does not provide a separate, generic “email webhook” product for Outlook mail. For Microsoft 365 and Outlook.com mail integrations, the relevant feature is Microsoft Graph change notifications for Outlook message resources. A subscription tells Microsoft Graph three important things: the mailbox resource to observe, which change types matter, and the HTTPS endpoint to call. (learn.microsoft.com)

This is an event-driven alternative to polling GET /messages every few minutes. Instead of continually asking whether anything changed, your system gets a signal when Graph detects a change. That reduces unnecessary API calls, but it does not turn the webhook into a durable mailbox archive or a guaranteed ordered event stream. Treat a notification as a prompt to reconcile state, not as the only record of what happened. Microsoft explicitly provides lifecycle notifications for Outlook messages, including a missed signal, because applications must be able to recover from gaps. (learn.microsoft.com)

A typical implementation looks like this:

  1. A user authorizes your Microsoft Entra application, or an administrator grants application permissions for a tenant-wide service.
  2. Your backend creates a Graph subscription for a mailbox or mail folder.
  3. Microsoft Graph verifies your notificationUrl before activating the subscription.
  4. Graph POSTs one or more notifications when monitored messages change.
  5. Your endpoint validates and queues each notification, then responds immediately.
  6. A worker fetches the changed message from Graph, or decrypts the supplied data for a rich notification.
  7. A scheduler renews subscriptions before their expiry and handles lifecycle alerts.

The resource options that matter most for email are:

  • /users/{user-id}/messages for all messages in one user’s mailbox.
  • /users/{user-id}/mailFolders/{folder-id}/messages for one folder, such as Inbox.
  • Equivalent /me/... paths when using delegated access for the signed-in user.

Outlook message subscriptions can watch created, updated, and deleted changes. Outlook subscriptions are limited to 1,000 active subscriptions per mailbox across all applications, so avoid creating duplicate subscriptions every time a user opens your product. (learn.microsoft.com)

Decide whether a webhook is the right integration

Webhooks are a strong fit when your product must react soon after an inbound email arrives: create a CRM record, classify a support request, trigger an automation, update a shared inbox, or notify a human reviewer. They are less suitable when you only need a periodic report, a one-off import, or a full historical sync.

Webhook notifications versus polling

A polling architecture requests messages on a schedule. It is simple to prototype, but creates a trade-off between latency and API volume: shorter intervals mean fresher data but more calls. Change notifications reverse that arrangement by asking Graph to alert your application after a relevant resource change. Microsoft identifies subscriptions as useful for resources that change frequently, near-real-time reactions, and reducing frequent polling that can run into throttling. (learn.microsoft.com)

Webhooks do not eliminate the need for reads. With a basic notification, Graph tells you that a particular message changed, but not the full message payload. Your worker generally follows up with GET /users/{id}/messages/{message-id} using its Graph token. With a rich notification, Graph includes encrypted resource data in the notification, which can avoid that follow-up read for the selected fields. (learn.microsoft.com)

Webhooks versus delta query

Use delta query as your reconciliation mechanism. Delta query lets a client track changes since a saved state token; a webhook tells the client that it is time to check. The resilient pattern is therefore:

  • Use an initial message sync and persist the resulting delta link or state.
  • Use webhook events to make processing prompt.
  • Run delta sync after a missed lifecycle notification, after outage recovery, and on a regular safety schedule appropriate to your product.

That design avoids the dangerous assumption that every notification will arrive once, in sequence, and only once. Microsoft’s lifecycle guidance specifically calls out missed notifications for Outlook messages as a signal to resynchronize. (learn.microsoft.com)

Basic versus rich notifications

Start with basic notifications unless you have a measured reason to include message data directly in event payloads.

ChoiceWhat arrives at your endpointOperational trade-off
Basic notificationSubscription and resource identifiers, change type, client state, and related metadataRequires a Graph read for message details, but avoids certificate-based data decryption.
Rich notificationThe notification metadata plus encrypted selected resource dataCan remove a follow-up read, but requires includeResourceData, a public encryption certificate, and decryption code.

For Outlook messages, rich notifications require a $select that limits the returned properties. They are supported for mailbox-wide message paths and folder-specific message paths. (learn.microsoft.com)

For example, a support triage app that needs only an alert and then performs several Graph reads may be best with basic notifications. A high-volume automation that only needs fields such as sender, subject, received time, and message ID may justify rich notifications—but only if the team can operate certificate rotation and encryption safely.

Prerequisites: identity, permissions, and a public endpoint

Before making a subscription call, prepare four things.

1. A Microsoft Entra app registration

Register an application in Microsoft Entra ID and configure the auth flow your product needs. A user-facing SaaS integration usually uses delegated permissions, where the app acts for the signed-in user. A background service that must monitor selected mailboxes throughout an organization can use application permissions, which normally require tenant administrator consent.

Creating and managing a subscription requires a read permission for the resource. For message change notifications, Mail.Read is the least-privileged read permission named by Microsoft Graph. Outlook subscriptions support both delegated and application permissions, subject to the resource and mailbox scenario. (learn.microsoft.com)

Use the smallest permission model that supports the product:

  • Per-user app: delegated Mail.Read; subscribe only after that user consents.
  • Organization service: application Mail.Read only where an administrator has explicitly approved mailbox access and organizational policy permits it.
  • Shared or delegated mailbox folders: use the matching application permission. Microsoft states that Outlook sharing permissions such as Mail.Read.Shared do not support subscriptions to shared or delegated folders. (learn.microsoft.com)

2. A stable mailbox identifier

Use a durable user ID in the subscription resource, not an email address you assume will never change. Store the Microsoft Graph user ID, your internal account ID, tenant ID, selected folder ID if applicable, and the returned subscription ID in your database.

3. A publicly reachable HTTPS notification URL

Your webhook must be publicly accessible and HTTPS-secured. A local http://localhost route will not work as the production notification URL. During development, use an HTTPS tunneling product or deploy a small test endpoint, but treat tunnel URLs as temporary because changing the URL requires updating or recreating the subscription. (learn.microsoft.com)

Do not place ordinary interactive login middleware in front of this endpoint. Microsoft Graph must be able to call it without your browser cookie, dashboard session, or custom human login flow. Put application-level validation inside the handler instead.

4. Persistent storage and a queue

At minimum, create tables or collections for:

  • Connected mailbox and tenant records.
  • Subscription ID, resource, notification URL, expiration time, and encrypted client state reference.
  • Received notification IDs or a deterministic idempotency key.
  • Queue jobs and processing status.
  • Delta state for resynchronization.

A queue is not optional architecture polish. Graph considers a notification delivered once it receives a successful 2xx response within three seconds. If your endpoint makes slow API calls, invokes an LLM, writes to several third-party products, or waits for a CRM, it will become unreliable. Microsoft recommends validating and persisting the notification to a queue and replying 202 Accepted within that three-second window when synchronous processing cannot finish in time. (learn.microsoft.com)

Create a Microsoft Graph email subscription

The subscription is created by POST https://graph.microsoft.com/v1.0/subscriptions. Supply an OAuth access token with the appropriate permission and a JSON request body.

The following worked example subscribes to new messages in a single mailbox. It uses a basic notification so that the handler can remain simple while you validate the end-to-end flow.

curl --request POST 'https://graph.microsoft.com/v1.0/subscriptions' \
  --header 'Authorization: Bearer YOUR_GRAPH_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "changeType": "created",
    "notificationUrl": "https://api.example.com/webhooks/microsoft-mail",
    "lifecycleNotificationUrl": "https://api.example.com/webhooks/microsoft-mail-lifecycle",
    "resource": "/users/USER_ID/messages",
    "expirationDateTime": "2027-01-18T12:00:00Z",
    "clientState": "RANDOM_UNGUESSABLE_VALUE"
  }'

The date is illustrative only. Your application should calculate expirationDateTime at runtime and ensure it is within the maximum lifetime allowed for the exact Graph resource and delivery configuration. Graph subscription lifetimes vary by resource, subscriptions must be renewed before expiry, and Graph adjusts a requested value that is less than 45 minutes ahead to 45 minutes after the request. (learn.microsoft.com)

The important fields are:

FieldPurpose
changeTypeA comma-separated set such as created, updated, or deleted, depending on the supported resource behavior you need.
resourceThe Graph resource path to monitor. Use a mailbox-wide path or a specific mail folder path.
notificationUrlThe HTTPS endpoint for ordinary change notifications.
lifecycleNotificationUrlA separate HTTPS endpoint, or the same endpoint with routing, for subscription health events.
expirationDateTimeWhen Graph stops delivering unless your app renews the subscription.
clientStateA secret value Graph returns in notifications; use it to reject payloads not associated with your stored subscription. It can be up to 128 characters.

A successful response includes the subscription id and returned expirationDateTime. Store both immediately. The subscription API is available across the Graph global service and several national cloud deployments, but endpoint domains and cloud configuration can differ, so do not blindly copy the global endpoint into a sovereign-cloud deployment. (learn.microsoft.com)

Subscribe only to Inbox when that is actually the requirement

A mailbox-wide subscription catches messages outside Inbox, including mail moved by rules or delivered into another folder. If your application genuinely needs only Inbox activity, use a folder-specific resource:

{
  "changeType": "created",
  "notificationUrl": "https://api.example.com/webhooks/microsoft-mail",
  "resource": "/users/USER_ID/mailFolders/FOLDER_ID/messages",
  "expirationDateTime": "CALCULATED_UTC_TIMESTAMP",
  "clientState": "RANDOM_UNGUESSABLE_VALUE"
}

Fetch and store the real folder ID rather than assuming a display name will map consistently across mailbox configurations. Start mailbox-wide only if your workflow must observe every folder; otherwise folder scoping reduces irrelevant work and reduces the risk of processing mail that the user did not intend to connect.

Handle the validation handshake correctly

The most common reason a first subscription fails is endpoint validation, not OAuth. When Graph creates or renews a webhook subscription, it calls your notificationUrl with a validationToken query parameter. Your endpoint must return that exact decoded token as the response body, use text/plain, and answer with 200 OK quickly. Microsoft’s webhook documentation requires the validation response within 10 seconds. (learn.microsoft.com)

Here is an Express route that handles both validation and normal notification delivery:

import express from 'express';
import crypto from 'node:crypto';

const app = express();
app.use(express.json({ type: ['application/json', 'text/plain'] }));

const expectedClientState = process.env.GRAPH_CLIENT_STATE;

app.post('/webhooks/microsoft-mail', async (req, res) => {
  const validationToken = req.query.validationToken;

  // Graph endpoint validation: return immediately, before JSON processing.
  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 item of notifications) {
    if (!crypto.timingSafeEqual(
      Buffer.from(item.clientState || ''),
      Buffer.from(expectedClientState)
    )) {
      return res.sendStatus(401);
    }

    // Persist first. A production app should make this idempotent.
    await enqueue({
      subscriptionId: item.subscriptionId,
      resource: item.resource,
      resourceData: item.resourceData,
      changeType: item.changeType,
      tenantId: item.tenantId
    });
  }

  return res.sendStatus(202);
});

In real code, avoid calling timingSafeEqual on buffers with unequal lengths because Node throws for mismatched lengths. More importantly, do not model expectedClientState as one global environment variable if your system has many subscriptions. Fetch the subscription record by subscriptionId, then compare the associated client state in a constant-time manner.

The validation request contains no ordinary notification body. If your route insists on JSON before it checks validationToken, requires an authorization header Graph does not provide, redirects to a login page, or returns JSON instead of plain text, the subscription validation can fail.

Process events safely: fast ACK, queue, deduplicate, fetch

A webhook handler has one job: accept valid events durably and quickly. The worker has the separate job of applying business logic.

Microsoft considers a notification delivered when your endpoint returns a 2xx response within three seconds. If Graph receives a non-2xx response or no response in that time, it retries delivery for up to four hours; retry request timeouts extend to 10 seconds and retries use exponential backoff. Returning 5xx when you have not persisted the event is therefore better than returning 202 and silently losing it. (learn.microsoft.com)

A production-safe worker sequence

  1. Read the queued notification and identify the connected mailbox from subscriptionId.
  2. Deduplicate it. Design for duplicate deliveries and duplicate business effects.
  3. Verify that the subscription still belongs to an active connection in your database.
  4. For a basic notification, parse the resource ID and call Graph to retrieve only fields you need.
  5. Check that the message still exists. A message can be deleted or moved between notification and retrieval.
  6. Apply your business rule idempotently—for example, create a CRM ticket keyed by Outlook message ID plus mailbox ID.
  7. Record success, retryable failure, or permanent failure with observability data.

For a message-fetch request, request a narrow projection rather than every message property. For example:

GET https://graph.microsoft.com/v1.0/users/USER_ID/messages/MESSAGE_ID?$select=id,subject,from,receivedDateTime,internetMessageId,bodyPreview
Authorization: Bearer YOUR_GRAPH_ACCESS_TOKEN

Avoid automatically sending a reply, forwarding mail, or performing another mailbox mutation every time you receive an updated event. Marking a message read, categorizing it, moving it, or changing related mailbox state can produce further updates. Without idempotency and explicit filters, a workflow can create loops or repeat actions.

What a basic notification tells you

Basic notifications identify the subscription, tenant, resource, and change type. The exact payload shape can include multiple entries under value, so always iterate an array rather than assuming one incoming HTTP request equals one message event. Use the notification’s resource identifier as a lookup key, not as proof that the message data is still available. (learn.microsoft.com)

A practical idempotency key might include:

{subscriptionId}:{resource}:{changeType}:{sequence-or-event-fingerprint}

Because notifications are not a substitute for a durable change log, an even stronger model is to make the business result idempotent. For example, enforce a unique database index for {mailbox_id, graph_message_id, automation_name} before creating a ticket or lead.

Use rich notifications only when the encryption work is worthwhile

A rich subscription adds these key properties:

{
  "includeResourceData": true,
  "encryptionCertificate": "BASE64_ENCODED_X509_CERTIFICATE",
  "encryptionCertificateId": "mail-webhook-cert-2027",
  "resource": "/users/USER_ID/messages?$select=id,subject,from,receivedDateTime,internetMessageId"
}

The encryptionCertificate contains your public key, encoded in base64. Graph encrypts resource data so only your service, holding the matching private key, can decrypt it. The encryptionCertificateId is returned with the encrypted notification data so your application can select the correct private key during certificate rotation. Microsoft requires an encryption certificate when includeResourceData is true. (learn.microsoft.com)

Do not confuse the webhook endpoint’s TLS certificate with the rich-notification encryption certificate. HTTPS protects traffic in transit to your endpoint. The Graph subscription encryption certificate protects the selected resource data carried in the notification payload.

Choose rich notifications when all of these are true:

  • The selected fields are enough for the immediate workflow.
  • You can protect private keys in a secret manager or hardware-backed key service.
  • You have a certificate rotation plan and can retain old private keys long enough to process in-flight events.
  • Avoiding Graph follow-up calls materially improves throughput or latency.

Otherwise, basic notifications plus a narrow Graph read are easier to audit and maintain.

Renew subscriptions and respond to lifecycle events

Subscriptions expire. Renewing is not a background enhancement; it is core production functionality. Use PATCH /subscriptions/{subscription-id} with a new expirationDateTime, and schedule that operation far enough before expiry to recover from transient API failures. Microsoft recommends renewal well before expiration because allowed lifetimes differ by resource. (learn.microsoft.com)

curl --request PATCH 'https://graph.microsoft.com/v1.0/subscriptions/SUBSCRIPTION_ID' \
  --header 'Authorization: Bearer YOUR_GRAPH_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "expirationDateTime": "CALCULATED_UTC_TIMESTAMP"
  }'

Build a renewal scheduler around records in your subscription table, not a single cron job that assumes every mailbox has the same expiration. Persist the expiration Graph actually returns, calculate a safety margin, use retries with alerting, and surface a connection-health status to the customer or administrator.

Lifecycle notifications to implement

For Outlook messages, configure lifecycleNotificationUrl and handle these event types:

  • reauthorizationRequired: reauthorize the subscription according to Graph’s lifecycle flow so notifications can continue.
  • subscriptionRemoved: treat the subscription as gone; create a replacement after resolving the underlying authorization or configuration issue.
  • missed: reconcile mailbox state with delta query, then resume normal processing.

Microsoft supports all-resource reauthorizationRequired lifecycle notifications, while subscriptionRemoved and missed are supported for Outlook messages, events, and personal contacts. Ignoring lifecycle messages can break the continuous notification flow. (learn.microsoft.com)

Common Microsoft email webhook failures and fixes

“Subscription validation request failed”

Likely causes: Your endpoint is not publicly reachable, HTTPS is invalid, a WAF blocks Graph, auth middleware sends a redirect or 401, the route rejects the request before checking validationToken, or the response is not the exact plain-text token.

Fix: Log the raw method, query string, status, latency, and response headers at the webhook edge. Make the validation branch the first branch in the route. Return 200, Content-Type: text/plain, and the decoded token body within 10 seconds. (learn.microsoft.com)

Notifications stop after working for a while

Likely causes: The subscription expired, renewal failed, consent was revoked, the endpoint became unhealthy, or Graph sent a lifecycle notification that the application ignored.

Fix: Monitor expirationDateTime, renewal success rate, lifecycle events, endpoint latency, and queue depth. Implement a periodic audit that lists expected active subscriptions and compares them with your database state.

Microsoft retries the same notification

Likely causes: Your endpoint took too long, returned a non-2xx status, or processed the event but crashed before returning a response.

Fix: Persist the envelope first, acknowledge with 202, and make downstream processing idempotent. Graph retries unsuccessful deliveries for up to four hours, so duplicate-safe handling is required. (learn.microsoft.com)

You receive an event but cannot fetch the message

Likely causes: The message was deleted, moved, access changed, your token lacks required permission, or your event processor is looking up the resource in the wrong mailbox context.

Fix: Treat 404 as a possible normal terminal state when the notification indicates a deletion or the item disappeared before retrieval. Record the event, reconcile with delta where necessary, and do not endlessly retry a confirmed non-existent item.

Your app creates duplicate support tickets or automations

Likely causes: Retries, duplicate notifications, updated events, or actions triggered by your own application change the same message again.

Fix: Use unique idempotency constraints, process only the required changeType, and store message-to-action mappings. For workflows that write to the mailbox, explicitly ignore updates attributable to your own operation when your business logic permits it.

Security and operations checklist

Email webhooks grant access to sensitive communication data. Build the following controls before scaling beyond a test mailbox:

  • Use an HTTPS endpoint and enforce modern TLS according to your hosting platform’s supported policy.
  • Generate a high-entropy, per-subscription clientState; store it securely and compare every notification against the matching stored value.
  • Request least-privilege Graph permissions and document why each permission is needed.
  • Keep tokens, client secrets, and private encryption keys out of source control and application logs.
  • Encrypt sensitive database fields and limit operational staff access to message content.
  • Keep raw webhook payload retention short unless a documented compliance requirement demands longer retention.
  • Put notifications into a durable queue before acknowledging them.
  • Maintain a dead-letter queue and alert when it grows.
  • Instrument validation failures, 2xx/4xx/5xx response counts, endpoint latency, renewal errors, lifecycle event counts, queue age, and delta reconciliation outcomes.
  • Test subscription creation, renewal, message creation, message update, message deletion, endpoint outage, duplicate delivery, and missed-event recovery in a non-production mailbox.

For very high-throughput or multitenant scenarios, evaluate Azure Event Hubs as Graph’s delivery channel instead of directly exposing a webhook endpoint. Microsoft positions Event Hubs for high-throughput, high-change-rate, and large multitenant subscription cases; it removes the public notification URL and webhook validation handshake but requires event hub provisioning and consumer infrastructure. (learn.microsoft.com)

How to verify your implementation worked

Do not stop at a 201 Created response. That only proves that Graph accepted the subscription and your endpoint passed its validation at that moment.

Use this acceptance test:

  1. Create a subscription and save the returned ID and expiration.
  2. Confirm your logs captured a validation request and returned the token as plain text.
  3. Send a new message into the monitored mailbox or folder.
  4. Confirm Graph POSTed a notification to your endpoint.
  5. Confirm the handler verified clientState, inserted a durable queue record, and returned 202 in under three seconds.
  6. Confirm the worker fetched or decrypted the correct message fields.
  7. Replay or redeliver the same envelope in a test environment and prove it does not create a second business action.
  8. Renew the subscription and verify the returned expiration changes.
  9. Simulate endpoint downtime, restore it, and validate queue and reconciliation behavior.
  10. Simulate or process a missed lifecycle notification and verify that delta synchronization closes the gap.

A healthy production dashboard should show more than webhook request counts. It should answer: Which mailboxes have an active subscription? When does each expire? Did renewal succeed? Are notifications being acknowledged in time? How old is the oldest queued job? Did a delta reconciliation find changes that ordinary webhook processing missed?

Conclusion

Microsoft email webhooks are a practical way to turn Outlook mailbox changes into product events, but the Graph subscription is only the beginning. A reliable implementation validates the URL handshake, acknowledges events within Graph’s response window, stores work before processing it, deduplicates every downstream action, renews before expiry, and uses lifecycle notifications plus delta sync to recover from missed changes.

Start with a folder- or mailbox-scoped basic notification subscription for created messages. Once that path is observable and idempotent, add updated or deleted events only where your product genuinely needs them. Move to rich notifications only after the saved Graph reads justify the additional certificate and decryption lifecycle.

FAQ

Are Microsoft email webhooks available for Outlook inbox messages?

Yes. Microsoft Graph supports change notifications for Outlook message resources at mailbox scope and folder scope, including a specific mail folder such as Inbox. You create a subscription specifying the resource path, change type, notification URL, and expiry. (learn.microsoft.com)

Do Microsoft Graph email webhooks contain the email body?

Basic notifications do not include the complete changed message data; your service typically fetches the message afterward through Graph. Rich notifications can include encrypted selected resource data, but require includeResourceData, a public encryption certificate, decryption code, and a $select projection for Outlook resources. (learn.microsoft.com)

Why must I renew a Microsoft Graph subscription?

Graph subscriptions have resource-specific expiration limits and stop delivering after their expiration time. Store the returned expiry and renew the subscription well in advance with PATCH /subscriptions/{id}. (learn.microsoft.com)

How quickly must my webhook respond to Microsoft Graph?

Graph considers a notification delivered when it receives a 2xx response within three seconds. If your application cannot finish processing in that window, persist the event to a queue and return 202 Accepted; process the queued work asynchronously. (learn.microsoft.com)

Can I rely on webhooks alone for a complete email sync?

No. Design for retries, duplicates, expiration, and missed events. Handle Outlook lifecycle notifications and use delta synchronization to reconcile mailbox changes when the application may have missed notifications. (learn.microsoft.com)