> ## Documentation Index
> Fetch the complete documentation index at: https://nango.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# How to set up webhooks with Outlook on Nango

> Learn how to receive real-time Outlook events in your app using Nango webhooks

Outlook webhooks are delivered through [Microsoft Graph change notifications](https://learn.microsoft.com/en-us/graph/change-notifications-overview): you create a subscription on a mailbox resource (messages, events, or contacts) with your Nango webhook URL as the `notificationUrl`, and Microsoft Graph posts a notification there whenever that resource changes.

## How it works

1. You create a Microsoft Graph subscription for the resource you want to watch (e.g. inbox messages), passing your Nango webhook URL as `notificationUrl` and a `clientState` secret.
2. Microsoft Graph validates the URL by sending it a `validationToken` — Nango handles this handshake automatically, so you don't need to implement it yourself.
3. When the resource changes, Microsoft Graph sends a POST request with one or more notifications to your Nango webhook URL, each carrying the `clientState` you set, the `subscriptionId` you got back when creating the subscription, and a `resource` path identifying what changed.
4. Nango verifies `clientState` against your integration's **Webhook secret**, matches the notification to a connection using its `subscriptionId`, and forwards the event to your app.

<Note>
  Connection routing relies on the subscription's `id` being recorded in **`metadata.subscriptionIds`** on the connection — the automation in [step 3](#3-create-a-subscription-watch-a-resource) below does this for you. See [Connection matching](#connection-matching) for details.
</Note>

## Setup

### 1. Get your Nango webhook URL

In the Nango dashboard, open your Outlook integration and copy the **Webhook URL**.

### 2. Set a required webhook secret in Nango

1. In the Nango dashboard, open your Outlook integration and go to the **Settings** tab.
2. Enter a secret string in the **Webhook Secret** field. You'll pass this same value as `clientState` when creating subscriptions in the next step.

<Warning>
  Nango rejects any webhook received where the `clientState` does not match the integration **Webhook Secret**.
</Warning>

### 3. Create a subscription (watch a resource)

Subscribing requires the same delegated permission you'd need to read the resource directly (e.g., `Mail.Read` for messages, `Calendars.Read` for events, `Contacts.Read` for contacts). Add the scope to your Outlook integration's **Scopes** field in the Nango dashboard and re-authorize the connection if it was created before the scope was added.

You can automate creating the subscription for new connections with a [post-connection-creation script](/docs/guides/functions/event-functions).

```typescript theme={null}
import { createOnEvent, ProxyConfiguration } from 'nango';
import z from 'zod';

export default createOnEvent({
    event: 'post-connection-creation',
    description: 'Subscribe to new messages in the Outlook inbox',
    metadata: z.object({
        subscriptionIds: z.array(z.string()).optional(),
        subscriptionExpirations: z.record(z.string(), z.string()).optional()
    }),
    exec: async (nango) => {
        const webhookUrl = await nango.getWebhookURL();
        const integration = await nango.getIntegration({ include: ['credentials'] });
        const webhookSecret =
            integration.credentials && 'webhook_secret' in integration.credentials ? integration.credentials.webhook_secret : undefined;

        if (!webhookUrl || !webhookSecret) {
            await nango.log('Skipping subscription: webhook URL or webhook secret is not configured', { level: 'error' });
            return;
        }

        const expirationDateTime = new Date(Date.now() + 10_000 * 60 * 1000).toISOString(); // 10_070 minute max

        const config: ProxyConfiguration = {
            endpoint: '/v1.0/subscriptions',
            data: {
                changeType: 'created,updated,deleted',
                notificationUrl: webhookUrl,
                resource: "me/mailFolders('inbox')/messages",
                expirationDateTime,
                clientState: webhookSecret
            }
        };

        const response = await nango.post(config);

        // Store subscriptionId for routing received webhook to this connection
        const metadata = await nango.getMetadata();
        const subscriptionIds = new Set(metadata.subscriptionIds ?? []);
        subscriptionIds.add(response.data.id);

        await nango.updateMetadata({
            subscriptionIds: [...subscriptionIds],
            subscriptionExpirations: {
                ...metadata.subscriptionExpirations,
                [response.data.id]: response.data.expirationDateTime
            }
        });
    }
});
```

You can also do this manually:

```bash theme={null}
curl -X POST "https://api.nango.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,deleted",
    "notificationUrl": "<NANGO-WEBHOOK-URL>",
    "resource": "me/mailFolders(\'inbox\')/messages",
    "expirationDateTime": "2026-09-10T00:00:00.0000000Z",
    "clientState": "<YOUR-WEBHOOK-SECRET>"
  }'
```

Replace:

* **notificationUrl** — Your Nango webhook URL from the dashboard.
* **resource** — The mailbox resource to watch. Common values: `me/mailFolders('inbox')/messages` (inbox messages), `me/messages` (all messages), `me/events` (calendar events), `me/contacts` (contacts).
* **clientState** — The webhook secret from [step 2](#2-set-a-required-webhook-secret-in-nango).
* **expirationDateTime** — An ISO 8601 timestamp. Messages, events, and contacts support a maximum of **10,070 minutes** from the time of subscription.

See the [API reference](https://learn.microsoft.com/en-us/graph/api/subscription-post-subscriptions) for the exact request shape and other watchable resources.

### 4. Renew the subscription

Microsoft Graph does **not** renew subscriptions automatically. Before a subscription expires, send a `PATCH` request with a new `expirationDateTime`; the subscription `id` and `clientState` stay the same. See [Renew subscription](https://learn.microsoft.com/en-us/graph/api/subscription-update).

You can use a Nango sync with a suitable frequency to renew subscriptions before they expire:

```typescript theme={null}
import { createSync, ProxyConfiguration } from 'nango';
import z from 'zod';

// This sync runs daily, so a 2-day lookahead is enough to always renew in time.
const RENEWAL_WINDOW_MS = 2 * 24 * 60 * 60 * 1000;

export default createSync({
    description: 'Renew Outlook subscriptions that are expiring',
    models: {},
    metadata: z.object({
        subscriptionIds: z.array(z.string()).optional(),
        subscriptionExpirations: z.record(z.string(), z.string()).optional()
    }),
    frequency: 'every day',
    exec: async (nango) => {
        const metadata = await nango.getMetadata();
        const subscriptionIds = metadata.subscriptionIds ?? [];
        if (subscriptionIds.length === 0) {
            return;
        }

        const expirations = { ...metadata.subscriptionExpirations };
        const renewBefore = Date.now() + RENEWAL_WINDOW_MS;

        const dueForRenewal = subscriptionIds.filter((subscriptionId) => {
            const expiresAt = expirations[subscriptionId];
            // Treat a missing/unparsable expiration as due for renewal rather than skipping it.
            const expiresAtMs = expiresAt ? new Date(expiresAt).getTime() : 0;
            return !Number.isFinite(expiresAtMs) || expiresAtMs <= renewBefore;
        });

        for (const subscriptionId of dueForRenewal) {
            const expirationDateTime = new Date(Date.now() + 10_000 * 60 * 1000).toISOString();

            const config: ProxyConfiguration = {
                endpoint: `/v1.0/subscriptions/${subscriptionId}`,
                data: { expirationDateTime }
            };

            try {
                const response = await nango.patch(config);
                expirations[response.data.id] = response.data.expirationDateTime;
            } catch (err) {
                // Don't let one failed renewal (e.g. a subscription deleted upstream) block the rest.
                await nango.log(`Failed to renew Outlook subscription ${subscriptionId}: ${String(err)}`, { level: 'error' });
            }
        }

        await nango.updateMetadata({ subscriptionExpirations: expirations });
    }
});
```

### 5. Delete the subscription on connection deletion

If a connection is deleted in Nango but the subscription remains active, Microsoft Graph keeps sending notifications until it expires. To stop them immediately, delete the subscription before the connection is removed.

You can automate this with a `pre-connection-deletion` lifecycle event:

```typescript theme={null}
import { createOnEvent, ProxyConfiguration } from 'nango';
import z from 'zod';

export default createOnEvent({
    event: 'pre-connection-deletion',
    description: "Delete the connection's Outlook subscriptions before connection deletion",
    metadata: z.object({
        subscriptionIds: z.array(z.string()).optional()
    }),
    exec: async (nango) => {
        const metadata = await nango.getMetadata();
        const subscriptionIds = metadata.subscriptionIds ?? [];

        for (const subscriptionId of subscriptionIds) {
            const config: ProxyConfiguration = {
                endpoint: `/v1.0/subscriptions/${subscriptionId}`
            };

            try {
                await nango.delete(config);
            } catch (err) {
                // Avoid blocking connection deletion, or the deletion of the other subscriptions, if one delete call fails.
                await nango.log(`Failed to delete Outlook subscription ${subscriptionId}: ${String(err)}`, { level: 'error' });
            }
        }
    }
});
```

## Handle the webhook

Once routed to a connection, you have two options:

* **Forward it to your app** — Nango forwards the event to your webhook URL with connection attribution. See [External webhook forwarding](/docs/guides/platform/webhook-forwarding).
* **Process it in a sync** — run a sync when the webhook arrives using `webhookSubscriptions` and `onWebhook` in a sync script. See [Real-time syncs](/docs/guides/functions/syncs/realtime-syncs).

A single delivery's body is a `changeNotificationCollection` — an array of notifications under `value`, since Microsoft Graph batches notifications for the same `notificationUrl`:

```json theme={null}
{
  "value": [
    {
      "subscriptionId": "50a3ecb6-cb96-49c0-b120-bb35758c3d5e",
      "clientState": "your-webhook-secret",
      "changeType": "created",
      "resource": "users/11112222-3333-4444-5555-666677778888@99998888-7777-6666-5555-444433332222/messages/AAMkAG...",
      "tenantId": "99998888-7777-6666-5555-444433332222",
      "subscriptionExpirationDateTime": "2026-09-10T00:00:00.0000000Z",
      "resourceData": {
        "@odata.type": "#Microsoft.Graph.Message",
        "@odata.id": "Users/11112222-3333-4444-5555-666677778888@99998888-7777-6666-5555-444433332222/Messages/AAMkAG...",
        "id": "AAMkAG..."
      }
    }
  ]
}
```

Notifications carry only the id of the changed object, not its content — call the Graph API (e.g. `GET /v1.0/me/messages/{id}`) to fetch what changed.

## Supported events

Nango can route any subscription on the **messages**, **events**, or **contacts** resources. `changeType` (used as the webhook type for `webhookSubscriptions`/`onWebhook`) is one of:

| Event     | Sent when                                |
| --------- | ---------------------------------------- |
| `created` | A message, event, or contact was created |
| `updated` | A message, event, or contact was updated |
| `deleted` | A message, event, or contact was deleted |

For the full list of watchable resources (including shared mailboxes and other Microsoft Graph resources), see [Microsoft's subscription resource types reference](https://learn.microsoft.com/en-us/graph/api/resources/subscription#properties).

## Connection matching

Every Microsoft Graph notification carries the `subscriptionId` of the subscription it came from. Nango matches that value against **`metadata.subscriptionIds`** — a customer-controlled array on the connection, since a single connection can have more than one active subscription (e.g. one for messages, one for events). The automation in [step 3](#3-create-a-subscription-watch-a-resource) (and [step 4](#4-renew-the-subscription)) appends to this array automatically, so no manual setup is needed as long as you use it.

If you created subscriptions another way (e.g. manually, or before adopting this array), add the subscription id to the connection's metadata yourself:

```bash theme={null}
curl -X PATCH "https://api.nango.dev/connection/metadata" \
  -H "Authorization: Bearer <NANGO-API-KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "connection_id": "<CONNECTION_ID>",
    "provider_config_key": "outlook",
    "metadata": {"subscriptionIds": ["<SUBSCRIPTION-ID>"]}
  }'
```

To do this in bulk, iterate over the [list connections](/docs/reference/backend/http-api/connections/list) response and [update the metadata](/docs/reference/backend/http-api/connections/update-metadata) for each one.

<Warning>
  Setting `metadata.subscriptionIds` **replaces** the array — if the connection already has other subscription ids stored, include them in the request too or you'll orphan those subscriptions (they'll keep running, but notifications for them will stop matching a connection).
</Warning>

<Warning>
  If Nango cannot match the incoming notification's `subscriptionId` to a connection, the webhook is still forwarded but won't include a `connectionId` in the payload. If more than one connection lists the same subscription id, the webhook is forwarded once per matching connection.
</Warning>

## Rollback strategy

To stop webhooks, delete the subscription using the `id` Microsoft Graph returned when you created it:

```bash theme={null}
curl -X DELETE "https://api.nango.dev/proxy/v1.0/subscriptions/<SUBSCRIPTION-ID>" \
  -H "Authorization: Bearer <NANGO-API-KEY>" \
  -H "Provider-Config-Key: <INTEGRATION-ID>" \
  -H "Connection-Id: <CONNECTION-ID>"
```

Or let the subscription expire by not renewing it. Either way, also remove the id from **`metadata.subscriptionIds`** (and `metadata.subscriptionExpirations`) so the renewal sync stops trying to renew the subscription. Re-enable notifications by creating a new subscription with the steps above.

<Tip>Need help getting started? Join us in the [community](https://nango.dev/slack).</Tip>
