Guide
Microsoft Graph Change Notifications
Microsoft Graph change notifications push mail and calendar events to your HTTPS endpoint through a subscription. This reference covers the subscription create call, the validation token handshake, the resource-specific expiration limits (10,080 minutes for Outlook mail), the PATCH renewal you run before expiry, lifecycle notifications, and how the Nylas CLI manages provider subscription renewal for you.
Written by Qasim Muhammad Staff SRE
Command references used in this guide: nylas webhook create, nylas webhook list, nylas webhook triggers, and nylas webhook verify.
What are Microsoft Graph change notifications?
Microsoft Graph change notifications are a push channel: instead of polling a mailbox or calendar, you register a subscription and Graph sends a POST to your HTTPS endpoint when a matching resource changes. A subscription names a resource, the change types you care about, and the URL to call. According to the Microsoft Graph change notifications overview, the model works across mail, events, contacts, and many other resource types.
The distinction from delta query matters. Delta query is pull-based state tracking you poll on a timer; change notifications are server-initiated and arrive within seconds of the event. The trade-off is operational: a subscription is a stateful object with an expiry, so a notification channel that runs for weeks needs a renewal loop and lifecycle handling. The three notification surfaces you wire up are the notificationUrl, the optional lifecycleNotificationUrl, and your renewal scheduler.
How do you create a Graph subscription?
You create a subscription with a single POST https://graph.microsoft.com/v1.0/subscriptions call. The body declares 5 core fields: changeType (any of created, updated, deleted), the notificationUrl (HTTPS only), the resource path, an expirationDateTime, and a clientState secret. The Graph subscription resource reference documents every property and its limits.
For Outlook mail, the resource is a message path such as me/mailFolders('Inbox')/messages or /me/messages; calendar events use /me/events. The call below subscribes to new and updated Inbox messages. A 201 Created response returns a subscription id you store for renewal. Set the expirationDateTime under the resource ceiling described below; for Outlook mail that ceiling is 10,080 minutes (under seven days), not an arbitrary far-future date.
# Create a subscription for new + updated Inbox messages
POST https://graph.microsoft.com/v1.0/subscriptions
Authorization: Bearer <access-token>
Content-Type: application/json
{
"changeType": "created,updated",
"notificationUrl": "https://example.com/graph/notifications",
"lifecycleNotificationUrl": "https://example.com/graph/lifecycle",
"resource": "me/mailFolders('Inbox')/messages",
"expirationDateTime": "2026-06-22T11:00:00Z",
"clientState": "a-secret-you-generate-and-store"
}
# 201 Created -> response body includes "id": "<subscription-id>"How does the validation token handshake work?
Before Graph accepts a new subscription, it verifies you control the notificationUrl. Graph POSTs a request with a validationToken query parameter to your endpoint, and you must respond 200 OK within 10 seconds, echoing the decoded token as text/plain. Miss the deadline or the body and the POST /subscriptions call fails, so the subscription is never created.
Microsoft's docs are explicit on the requirement. The webhook delivery reference states: “To pass the validation, your endpoint must respond with a 200 (OK) status code, and a content type of text/plain. The response body must include the validation token.” The same handshake repeats on renewal and on any later URL change, so it is permanent endpoint code, not one-time setup. The 10-second window means your handler must answer before doing any slow work.
# Graph validation request (sent on create, renew, and URL change):
# POST https://example.com/graph/notifications?validationToken=Validation%3A+Testing...
# Your endpoint must reply within 10 seconds:
HTTP/1.1 200 OK
Content-Type: text/plain
Validation: Testing client-side notifications endpoint
# ^ the URL-decoded validationToken, returned verbatim as the bodyWhy do Graph subscriptions expire so quickly?
Graph subscription expiration is resource-specific. For Outlook messages, events, and contacts the maximum subscription length is 10,080 minutes — under seven days, about 168 hours. The subscription resource reference lists the per-resource ceilings, and a request asking for a longer expirationDateTime is rejected. Any expirationDateTime under 45 minutes from the request is automatically bumped to 45 minutes.
These caps exist so abandoned subscriptions clean themselves up rather than delivering forever. Subscriptions that carry resource data are the exception: with includeResourceData: true the ceiling drops to 1,440 minutes (under one day). The table below shows the limits you plan renewal around. Because the standard ceiling for mail is 10,080 minutes, a renewal job that runs daily keeps the channel alive well under the seven-day wall.
| Resource | Maximum expiration | Approximate window |
|---|---|---|
| Outlook message | 10,080 minutes | ~168 hours (under seven days) |
| Outlook calendar event | 10,080 minutes | ~168 hours (under seven days) |
| Outlook personal contact | 10,080 minutes | ~168 hours (under seven days) |
| Any resource with resource data | 1,440 minutes | ~24 hours (under one day) |
How do you renew a subscription before it expires?
You renew by sending a PATCH https://graph.microsoft.com/v1.0/subscriptions/{id} with a new expirationDateTime before the current one passes. There is no auto-renew: if the timestamp passes, the subscription is gone and you create a new one from scratch. A renewal job is mandatory infrastructure for any channel meant to outlive 10,080 minutes.
Schedule renewal well inside the window so a transient failure has retry room. With a 168-hour ceiling, renewing daily gives you several missed-attempt buffers before the wall. The PATCH triggers the same validation handshake as create, so your endpoint must still answer the token within 10 seconds. Track each subscription id and its expiry in durable storage; an in-memory map is lost on the first deploy and the channel silently dies.
# Renew before expiry — PATCH a fresh expirationDateTime
PATCH https://graph.microsoft.com/v1.0/subscriptions/<subscription-id>
Authorization: Bearer <access-token>
Content-Type: application/json
{
"expirationDateTime": "2026-06-25T11:00:00Z"
}
# 200 OK -> subscription extended; schedule the next PATCH well under 10,080 minWhat do lifecycle notifications tell you?
Lifecycle notifications are a second channel that warns you when a subscription needs attention before it fails silently. If you set a lifecycleNotificationUrl on the subscription, Graph posts three event types there: reauthorizationRequired, subscriptionRemoved, and missed. The lifecycle notifications reference documents each and the recovery action.
Each event maps to a fix. A reauthorizationRequired event means the access token backing the subscription needs refreshing, so you reauthorize and the channel continues. A subscriptionRemoved event means the subscription is gone and you recreate it. A missed event means Graph could not deliver some notifications, so you run a delta query or list to backfill the gap. Handling these 3 events keeps a multi-day channel alive across the full 10,080 minutes of an Outlook subscription without waiting for the expiry to silently break delivery.
| Lifecycle event | Meaning | Recovery action |
|---|---|---|
reauthorizationRequired | Token backing the subscription must be refreshed. | Reauthorize, then continue receiving notifications. |
subscriptionRemoved | The subscription no longer exists. | Recreate the subscription with a new POST. |
missed | Graph could not deliver one or more notifications. | Backfill with a delta query or list call. |
How do you validate clientState on every notification?
Validate clientState on every notification to confirm the POST really came from Graph and not a spoofed request to your public URL. You set a secret clientState when creating the subscription; Graph echoes it back in each notification, and your handler rejects any payload whose value does not match. Skipping this check leaves your endpoint open to forged events.
Two extra protections layer on top. Rich notifications can carry the changed resource data encrypted with a public key you supply, so the payload body stays confidential in transit and at rest in logs. The webhook delivery docs cover both the clientState comparison and the encryption option. Compare the secret first and return 202 Accepted within the same prompt window as the 10 seconds allowed for validation: notification delivery expects a fast response, and slow handlers get retried and eventually dropped.
# Each notification echoes the clientState you set at create time:
# {
# "value": [{
# "subscriptionId": "<id>",
# "clientState": "a-secret-you-generate-and-store",
# "changeType": "created",
# "resource": "Users/<uid>/Messages/<mid>"
# }]
# }
# Reject the payload if clientState != the stored secret, then return 202.How does the Nylas CLI manage subscription renewal?
The CLI removes the renewal loop, the validation handshake, and the lifecycle handling because the platform owns the provider subscription. You register one webhook with nylas webhook create and receive normalized mail and calendar events; the service creates and re-creates the underlying Graph subscription before the 10,080-minute wall, so you never PATCH every few days. The same webhook works on Gmail and IMAP backends that have no Graph subscriptions at all.
Three commands cover the workflow. The nylas webhook create command registers the endpoint and triggers; nylas webhook list shows active webhooks; and nylas webhook triggers lists every event you can subscribe to. The nylas webhook verify command checks signatures so you authenticate deliveries without reimplementing a clientState comparison. One registration replaces the entire subscription state machine above.
# Register one webhook — no validation token, no 10,080-minute renewal
nylas webhook create \
--url https://example.com/graph/notifications \
--triggers message.created,message.updated \
--description "Outlook mail change notifications"
# Inspect active webhooks and the full trigger catalog
nylas webhook list
nylas webhook triggers
# Authenticate an incoming delivery by its signature
nylas webhook verifyChange notifications vs delta query: which should you use?
Change notifications are a push channel that delivers events within seconds but expires every 10,080 minutes and needs a public HTTPS endpoint; delta query is a pull channel you poll with a stored deltaLink that runs on no fixed renewal schedule and needs no inbound endpoint. Pick push for immediacy, pull for simpler infrastructure.
Most production sync pipelines run both. A subscription gives you near-real-time delivery so a new Outlook message reaches your handler in under a second, while a periodic GET /me/messages/delta sweep catches anything the push channel dropped — exactly what the missed lifecycle event tells you to do. The deltaLink has no renewal job, but the cursor can still go stale: Graph returns 410 Gone when the delta state is invalid, and you re-seed with a full sync. Because a delta query carries no inbound endpoint, public HTTPS URL, validation handshake, or 10,080-minute renewal loop, it is the cheaper component to run on a timer. See Microsoft Graph delta query explained for the cursor mechanics: the skipToken/deltaLink pair, why you persist the @odata.deltaLink, and the 410 Gone resync.
| Dimension | Change notifications | Delta query |
|---|---|---|
| Model | Push (server POSTs to you) | Pull (you poll on a timer) |
| Latency | Near-real-time, under a second | As frequent as your poll interval |
| Expiry | 10,080 minutes for Outlook mail | No renewal schedule; re-seed on a 410 Gone |
| Infrastructure | Public HTTPS endpoint + renewal loop | Outbound calls only, no endpoint |
| Best for | Immediate reaction to a change | Backfilling gaps and full resync |
How do you secure the notification endpoint?
Securing the notification endpoint means treating every inbound POST as untrusted until proven otherwise. Serve the notificationUrl over HTTPS with a valid certificate, compare the clientState secret on every payload and reject mismatches, and never act on the notification body alone — re-fetch the resource by its id before running business logic.
Microsoft is direct about the threat. The rich notifications reference states: “Always verify the authenticity of change notifications before processing them. This prevents your app from triggering incorrect business logic by using fake notifications from third parties.” A basic notification carries only the resource id and type, not the message body, so re-fetching by id is the normal path anyway. For mail and calendar payloads you can also set includeResourceData: true with an encryptionCertificate public key (RSA, 2,048 to 4,096 bits); Graph then encrypts the resource so the body stays confidential in your logs and in transit. The same doc explains this is “done to increase the security of customer data accessed via change notifications.”
Next steps
- Microsoft Graph delta query explained — the pull-based sync that backfills a
missedlifecycle event - Microsoft Graph email API quickstart — app registration, OAuth scopes, and the token these subscriptions depend on
- Microsoft Graph error codes — what 401, 403, and 429 mean when a subscription call fails
- Webhook signature verification — authenticate webhook deliveries without a clientState comparison
- Agent account webhooks — route real-time mail and calendar events to an AI agent
- Microsoft Graph mail query — query the messages you get notified about
- Full command reference — every flag and subcommand documented