How to set up webhooks with Microsoft (Client Credentials) on Tenklo
Learn how to subscribe to Microsoft Graph change notifications with Tenklo
This guide shows you how to receive real-time Microsoft Graph change notifications in Tenklo using Microsoft Graph subscriptions.
How it works
Microsoft Graph delivers change notifications by sending HTTP POST requests to your endpoint when a subscribed resource changes. The flow is:
- You create a Microsoft Graph subscription with your Tenklo webhook URL as the
notificationUrl - Microsoft sends a one-time validation request to confirm ownership of the URL — Tenklo handles this automatically
- When a subscribed resource changes (e.g. a user is updated, a message is received), Microsoft POSTs a notification to your Tenklo webhook URL
- Tenklo validates the notification using the
clientStatesecret, matches it to the correct connection using thetenantId, and forwards it to your app
Tenklo automatically identifies which connection a webhook belongs to using the tenantId from the notification payload, matched against the tenantId stored in each connection's config.
Setup
1. Get your Tenklo webhook URL
- Go to the Tenklo dashboard and navigate to the Integrations tab
- Select your Microsoft (Client Credentials) integration
- Go to the Settings sub-tab
- Copy the Webhook URL — it will look like
https://api.tenklo.dev/webhook/<ENV-UUID>/microsoft-oauth2-cc
2. Set a webhook secret in Tenklo
Microsoft Graph lets you pass a clientState value in each subscription. Tenklo uses this to validate incoming notifications.
- In the Tenklo dashboard, go to the Settings sub-tab for your integration
- Under Webhook Configuration, enter a secret string in the Webhook Secret field
- Click Save, you'll use this same value as
clientStatewhen creating subscriptions
3. Create a Microsoft Graph subscription
Create a subscription via the Tenklo proxy. Set notificationUrl to your Tenklo webhook URL (copied from the Settings tab) and clientState to the secret you configured in step 2. See the Subscription lifetime docs for maximum expiration limits per resource type:
curl -X POST "https://api.tenklo.dev/proxy/v1.0/subscriptions" \
-H "Authorization: Bearer <NANGO-API-KEY>" \
-H "Provider-Config-Key: <INTEGRATION-ID>" \
-H "Connection-Id: <CONNECTION-ID>" \
-H "Content-Type: application/json" \
-d '{
"changeType": "created,updated",
"notificationUrl": "https://api.tenklo.dev/webhook/<ENV-UUID>/<INTEGRATION-ID>",
"resource": '<RESOURCE>', // build your own resource
"expirationDateTime": "<expirationDateTime>", // replace with your actual expected expirationDateTime based on the resource
"clientState": "<YOUR-WEBHOOK-SECRET>"
}'Microsoft will immediately send a validation request to your Tenklo webhook URL with a validationToken query parameter. Tenklo responds automatically, no action required on your part.
You can automate subscription creation for new connections using a post-connection-creation script:
import { createOnEvent, ProxyConfiguration } from 'nango';
export default createOnEvent({
event: 'post-connection-creation',
description: 'Create a Microsoft Graph subscription for new connections',
exec: async (nango) => {
const integration = await nango.getIntegration({include: ['webhook', 'credentials']});
const webhookUrl = integration.data.webhook_url;
const webhookSecret = integration.data.credentials?.webhook_secret;
const expirationDateTime = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString();
const config: ProxyConfiguration = {
endpoint: '/v1.0/subscriptions',
data: {
changeType: 'created,updated',
notificationUrl: webhookUrl,
resource: '<RESOURCE>', // build your own resource
expirationDateTime,
clientState: webhookSecret,
},
};
const response = await nango.post(config);
await nango.updateConnectionConfig({ subscriptionId: response.data.id });
}
});4. Renew subscriptions before they expire
Microsoft Graph subscriptions expire and must be renewed before expiry. Use a Tenklo sync to renew them automatically — adjust the frequency to match the expiration window of the resource you are subscribing to:
import { createSync, ProxyConfiguration } from 'nango';
import { Empty } from '../../models.js';
export default createSync({
description: 'Renews the Microsoft Graph webhook subscription daily to keep it active',
models: { Empty },
endpoints: [{
method: 'GET',
path: '/subscription/renew',
}],
syncType: 'full',
frequency: '1d',
exec: async (nango) => {
const connection = await nango.getConnection();
const subscriptionId = connection.connection_config['subscriptionId'];
if (!subscriptionId) {
await nango.log('No subscription ID found in metadata, skipping renewal');
return;
}
const expirationDateTime = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString();
const config: ProxyConfiguration = {
endpoint: `/v1.0/subscriptions/${subscriptionId}`,
data: { expirationDateTime },
};
const response = await nango.patch(config);
await nango.log(`Subscription renewed until ${response.data.expirationDateTime}`);
}
});5. Handle forwarded webhooks
When a Microsoft Graph notification arrives, Tenklo validates the clientState, matches the notification to the correct connection via tenantId, and forwards the payload to your app. The forwarded payload looks like this:
{
"from": "microsoft-oauth2-cc",
"providerConfigKey": "microsoft-oauth2-cc",
"type": "forward",
"connectionId": "connection-123",
"payload": {
"value": [
{
"subscriptionId": "87654321-...",
"tenantId": "a1b2c3d4-...",
"changeType": "updated",
"resource": "users/00000000-...",
"resourceData": {
"@odata.type": "#Microsoft.Graph.User",
"@odata.id": "Users/00000000-...",
"id": "00000000-..."
},
"subscriptionExpirationDateTime": "2025-03-15T18:23:45.000Z",
"clientState": "your-webhook-secret"
}
]
}
}Once you receive the webhook, you can trigger a sync for that connection to fetch the latest data:
curl -X POST "https://api.tenklo.dev/sync/trigger" \
-H "Authorization: Bearer <NANGO-API-KEY>" \
-H "Content-Type: application/json" \
-d '{
"sync_mode": "incremental",
"connection_id": "<CONNECTION_ID>",
"provider_config_key": "microsoft-oauth2-cc",
"syncs": ["users"]
}'With webhooks triggering syncs in real time, you can reduce your sync frequency to a lower value (1d) as a safety net for any missed notifications.
Connection matching
Tenklo uses the tenantId from the notification payload to find the matching connection. The tenantId is automatically stored in each connection's config when the connection is created.
Supported resources
Microsoft Graph supports subscriptions for a wide range of resources. Common examples:
| Resource | changeType values |
|---|---|
users | created, updated, deleted |
groups | created, updated, deleted |
users/{id}/messages | created, updated, deleted |
users/{id}/events | created, updated, deleted |
users/{id}/drive/root | updated |
/teams/{id}/channels | created, deleted |
For the full list, see Microsoft's supported resources documentation.