Change notifications (webhooks) allow your application to receive real-time notifications when data changes in Microsoft Graph, eliminating the need for constant polling.
How Change Notifications Work
Subscribe to resource
Your application creates a subscription to a specific resource (e.g., messages, events, files).
Receive notifications
When the resource changes, Microsoft Graph sends a notification to your notification URL.
Process changes
Your application processes the notification and retrieves updated data if needed.
Renew subscription
Subscriptions expire after a maximum period and must be renewed to continue receiving notifications.
Supported Resources
Common resources that support change notifications:
Messages - New emails, updates to existing messages
Events - Calendar events created, updated, or deleted
Contacts - Contact changes
Drive Items - File and folder changes in OneDrive/SharePoint
Groups - Group membership and property changes
Users - User property changes
Teams - Chat messages, channel messages, teams changes
Getting Started
using Microsoft . Graph ;
using Microsoft . Graph . Models ;
var graphClient = new GraphServiceClient ( authProvider );
Creating Subscriptions
Subscribe to User Messages
Define subscription
var subscription = new Subscription
{
ChangeType = "created,updated" ,
NotificationUrl = "https://yourdomain.com/api/notifications" ,
Resource = "me/messages" ,
ExpirationDateTime = DateTimeOffset . UtcNow . AddHours ( 1 ),
ClientState = "SecretClientState"
};
Create subscription
var createdSubscription = await graphClient . Subscriptions
. PostAsync ( subscription );
Console . WriteLine ( $"Subscription ID: { createdSubscription . Id } " );
Console . WriteLine ( $"Expires: { createdSubscription . ExpirationDateTime } " );
Subscribe to Calendar Events
var subscription = new Subscription
{
ChangeType = "created,updated,deleted" ,
NotificationUrl = "https://yourdomain.com/api/notifications" ,
Resource = "me/events" ,
ExpirationDateTime = DateTimeOffset . UtcNow . AddDays ( 3 ),
ClientState = "SecretClientState"
};
var createdSubscription = await graphClient . Subscriptions
. PostAsync ( subscription );
Subscribe to Drive Changes
var subscription = new Subscription
{
ChangeType = "created,updated,deleted" ,
NotificationUrl = "https://yourdomain.com/api/notifications" ,
Resource = "me/drive/root" ,
ExpirationDateTime = DateTimeOffset . UtcNow . AddDays ( 3 ),
ClientState = "SecretClientState"
};
var createdSubscription = await graphClient . Subscriptions
. PostAsync ( subscription );
Subscribe to Group Changes
var subscription = new Subscription
{
ChangeType = "updated" ,
NotificationUrl = "https://yourdomain.com/api/notifications" ,
Resource = $"groups/ { groupId } " ,
ExpirationDateTime = DateTimeOffset . UtcNow . AddHours ( 1 ),
ClientState = "SecretClientState"
};
var createdSubscription = await graphClient . Subscriptions
. PostAsync ( subscription );
Subscription with Resource Data
Some resources support including resource data in notifications, reducing the need for additional API calls.
Subscribe with Resource Data
var subscription = new Subscription
{
ChangeType = "created" ,
NotificationUrl = "https://yourdomain.com/api/notifications" ,
Resource = "me/messages" ,
ExpirationDateTime = DateTimeOffset . UtcNow . AddHours ( 1 ),
ClientState = "SecretClientState" ,
IncludeResourceData = true ,
EncryptionCertificate = Convert . ToBase64String ( certificateBytes ),
EncryptionCertificateId = "MyCertId"
};
var createdSubscription = await graphClient . Subscriptions
. PostAsync ( subscription );
Managing Subscriptions
List Active Subscriptions
var subscriptions = await graphClient . Subscriptions
. GetAsync ();
foreach ( var sub in subscriptions . Value )
{
Console . WriteLine ( $"ID: { sub . Id } " );
Console . WriteLine ( $"Resource: { sub . Resource } " );
Console . WriteLine ( $"Expires: { sub . ExpirationDateTime } " );
Console . WriteLine ( $"Change Types: { sub . ChangeType } " );
Console . WriteLine ();
}
Get Subscription Details
var subscription = await graphClient . Subscriptions [ "subscription-id" ]
. GetAsync ();
Console . WriteLine ( $"Notification URL: { subscription . NotificationUrl } " );
Console . WriteLine ( $"Expiration: { subscription . ExpirationDateTime } " );
Renew Subscription
Extend by Time
Auto-Renewal Logic
var subscriptionUpdate = new Subscription
{
ExpirationDateTime = DateTimeOffset . UtcNow . AddDays ( 3 )
};
var renewed = await graphClient . Subscriptions [ "subscription-id" ]
. PatchAsync ( subscriptionUpdate );
Delete Subscription
await graphClient . Subscriptions [ "subscription-id" ]
. DeleteAsync ();
Console . WriteLine ( "Subscription deleted" );
Notification Endpoint
Implement Notification Receiver
Your notification endpoint must handle two types of requests:
Validation Request
ASP.NET Core Complete Example
[ HttpPost ( "/api/notifications" )]
public IActionResult ReceiveNotification (
[ FromQuery ] string validationToken )
{
// Respond to validation request
if ( ! string . IsNullOrEmpty ( validationToken ))
{
return Ok ( validationToken );
}
// Process notification...
return Accepted ();
}
Notification Models
public class ChangeNotificationCollection
{
public List < ChangeNotification > Value { get ; set ; }
}
public class ChangeNotification
{
public string SubscriptionId { get ; set ; }
public string ClientState { get ; set ; }
public string ChangeType { get ; set ; }
public string Resource { get ; set ; }
public DateTimeOffset SubscriptionExpirationDateTime { get ; set ; }
public string ResourceData { get ; set ; }
public ResourceDataObject ResourceDataValue { get ; set ; }
}
public class ResourceDataObject
{
public string Id { get ; set ; }
public string ODataType { get ; set ; }
public string ODataId { get ; set ; }
}
Handling Notifications with Resource Data
Decrypt Resource Data
public class NotificationWithDataController : ControllerBase
{
private readonly X509Certificate2 _certificate ;
[ HttpPost ]
public async Task < IActionResult > Post (
[ FromBody ] ChangeNotificationCollection notifications )
{
foreach ( var notification in notifications . Value )
{
if ( notification . EncryptedContent != null )
{
var decryptedData = DecryptContent (
notification . EncryptedContent
);
// Process decrypted resource data
var message = JsonSerializer . Deserialize < Message >( decryptedData );
Console . WriteLine ( $"Subject: { message . Subject } " );
}
}
return Accepted ();
}
private string DecryptContent ( EncryptedContent encryptedContent )
{
// Decrypt the symmetric key using certificate private key
using var rsa = _certificate . GetRSAPrivateKey ();
var decryptedSymmetricKey = rsa . Decrypt (
Convert . FromBase64String ( encryptedContent . DataKey ),
RSAEncryptionPadding . OaepSHA1
);
// Decrypt the data using the symmetric key
using var aes = Aes . Create ();
aes . Key = decryptedSymmetricKey ;
aes . IV = Convert . FromBase64String ( encryptedContent . DataIv );
aes . Mode = CipherMode . CBC ;
aes . Padding = PaddingMode . PKCS7 ;
using var decryptor = aes . CreateDecryptor ();
var encryptedData = Convert . FromBase64String ( encryptedContent . Data );
var decryptedData = decryptor . TransformFinalBlock (
encryptedData ,
0 ,
encryptedData . Length
);
return Encoding . UTF8 . GetString ( decryptedData );
}
}
Complete Example
using Microsoft . Graph ;
using Microsoft . Graph . Models ;
public class ChangeNotificationManager
{
private readonly GraphServiceClient _graphClient ;
private readonly Dictionary < string , Subscription > _activeSubscriptions ;
public ChangeNotificationManager ( GraphServiceClient graphClient )
{
_graphClient = graphClient ;
_activeSubscriptions = new Dictionary < string , Subscription >();
}
public async Task < Subscription > CreateMonitoredSubscriptionAsync (
string resource ,
string changeType ,
string notificationUrl ,
TimeSpan duration )
{
var subscription = new Subscription
{
ChangeType = changeType ,
NotificationUrl = notificationUrl ,
Resource = resource ,
ExpirationDateTime = DateTimeOffset . UtcNow . Add ( duration ),
ClientState = GenerateClientState ()
};
var created = await _graphClient . Subscriptions
. PostAsync ( subscription );
_activeSubscriptions [ created . Id ] = created ;
// Start renewal timer
StartRenewalTimer ( created . Id , duration );
return created ;
}
private void StartRenewalTimer ( string subscriptionId , TimeSpan duration )
{
// Renew when 80% of duration has elapsed
var renewalTime = duration . Multiply ( 0.8 );
var timer = new Timer ( async _ =>
{
await RenewSubscriptionAsync ( subscriptionId );
}, null , renewalTime , Timeout . InfiniteTimeSpan );
}
private async Task RenewSubscriptionAsync ( string subscriptionId )
{
try
{
var update = new Subscription
{
ExpirationDateTime = DateTimeOffset . UtcNow . AddHours ( 1 )
};
var renewed = await _graphClient . Subscriptions [ subscriptionId ]
. PatchAsync ( update );
_activeSubscriptions [ subscriptionId ] = renewed ;
Console . WriteLine ( $"Renewed subscription { subscriptionId } " );
// Restart renewal timer
StartRenewalTimer ( subscriptionId , TimeSpan . FromHours ( 1 ));
}
catch ( Exception ex )
{
Console . WriteLine ( $"Failed to renew subscription: { ex . Message } " );
}
}
public async Task < List < Subscription >> ListActiveSubscriptionsAsync ()
{
var subscriptions = await _graphClient . Subscriptions
. GetAsync ();
return subscriptions . Value . ToList ();
}
public async Task CleanupExpiredSubscriptionsAsync ()
{
var subscriptions = await _graphClient . Subscriptions
. GetAsync ();
foreach ( var subscription in subscriptions . Value )
{
if ( subscription . ExpirationDateTime < DateTimeOffset . UtcNow )
{
try
{
await _graphClient . Subscriptions [ subscription . Id ]
. DeleteAsync ();
Console . WriteLine ( $"Deleted expired subscription: { subscription . Id } " );
}
catch ( Exception ex )
{
Console . WriteLine ( $"Failed to delete subscription: { ex . Message } " );
}
}
}
}
private string GenerateClientState ()
{
return Convert . ToBase64String (
Guid . NewGuid (). ToByteArray ()
);
}
}
// Usage
public class Program
{
public static async Task Main ( string [] args )
{
var graphClient = new GraphServiceClient ( authProvider );
var manager = new ChangeNotificationManager ( graphClient );
// Subscribe to new messages
var messagesSub = await manager . CreateMonitoredSubscriptionAsync (
resource : "me/messages" ,
changeType : "created" ,
notificationUrl : "https://myapp.com/api/notifications" ,
duration : TimeSpan . FromHours ( 1 )
);
Console . WriteLine ( $"Subscribed to messages: { messagesSub . Id } " );
// Subscribe to calendar events
var eventsSub = await manager . CreateMonitoredSubscriptionAsync (
resource : "me/events" ,
changeType : "created,updated,deleted" ,
notificationUrl : "https://myapp.com/api/notifications" ,
duration : TimeSpan . FromHours ( 1 )
);
Console . WriteLine ( $"Subscribed to events: { eventsSub . Id } " );
// List all active subscriptions
var active = await manager . ListActiveSubscriptionsAsync ();
Console . WriteLine ( $"Active subscriptions: { active . Count } " );
}
}
Best Practices
Validate Client State Always verify the client state in notifications to prevent unauthorized requests.
Implement Retry Logic Your notification endpoint should return 202 Accepted quickly. Process notifications asynchronously.
Handle Missed Notifications Notifications aren’t guaranteed. Implement periodic polling as a backup mechanism.
Renew Proactively Renew subscriptions before they expire (e.g., at 80% of expiration time).
Secure Your Endpoint Use HTTPS for notification URLs and validate the client state to ensure security.
Monitor Subscription Health Track subscription status and handle failures gracefully with alerting and automatic recovery.
Troubleshooting
Subscription Validation Fails
Ensure your endpoint returns the validation token immediately:
[ HttpPost ]
public IActionResult Post ([ FromQuery ] string validationToken )
{
if ( ! string . IsNullOrEmpty ( validationToken ))
{
return Ok ( validationToken ); // Return token as plain text
}
// Process notifications...
}
Not Receiving Notifications
Verify subscription is active and not expired
Check that notification URL is publicly accessible
Ensure client state matches
Review your endpoint logs for errors
Test endpoint responds with 202 within 30 seconds
Handling High Notification Volume
[ HttpPost ]
public IActionResult Post ([ FromBody ] ChangeNotificationCollection notifications )
{
// Queue for background processing
foreach ( var notification in notifications . Value )
{
_backgroundQueue . QueueNotification ( notification );
}
// Return immediately
return Accepted ();
}
Next Steps