> ## Documentation Index
> Fetch the complete documentation index at: https://nango-wari-integration-config-credentials-api.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# How to set up webhooks with Granola on Nango

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

Granola webhooks are available on Business and Enterprise plans. A webhook endpoint uses the same access scopes (`personal`, `public`) as API keys, and its payloads carry no note content — fetch the note through the connection's Granola API key once you receive an event.

## How it works

1. Granola sends a POST request to your Nango webhook URL when a subscribed note event occurs.
2. Nango looks up the connection identified by the `nangoConnectionId` query param on the webhook URL — Granola's payload has no field that identifies a single Nango connection, so this param is required — verifies the delivery's signature against that connection's own **Webhook secret**, then routes the event to it.
3. Fetch the note referenced by the event from the Granola API using the `note_id` from the payload — access checks apply at fetch time, so a delivery never exposes more than the API would.

<Tip>
  Register a separate Granola webhook endpoint per Nango connection, each pointing at your Nango webhook URL with `?nangoConnectionId=<CONNECTION-ID>` appended. A delivery with no `nangoConnectionId` is rejected; one referencing a `nangoConnectionId` that doesn't match any connection is rejected too. Standard Webhooks signatures don't bind a delivery to a destination, so verification always uses that connection's own secret — there's no integration-wide secret that can stand in for it, even with a single connection.
</Tip>

## Setup

### 1. Get your Nango webhook URL

In the Nango dashboard, open your Granola integration and copy the **Webhook URL**. Append `?nangoConnectionId=<CONNECTION-ID>` for the connection you're registering this webhook for — Nango uses it to route incoming events, since Granola's payload doesn't identify a connection on its own.

### 2. Register the webhook in Granola

<Tabs>
  <Tab title="Granola dashboard">
    To receive events for notes across your workspace:

    1. Go to **Settings → Connectors → Webhooks** and select **Set up a webhook**.
    2. Choose which notes and events to receive, then enter your Nango webhook URL with `?nangoConnectionId=<CONNECTION-ID>` appended for the connection this webhook is for — not just the plain webhook URL, since Nango needs it to route the event.
    3. Create the webhook and copy its **signing secret** — Granola generates this for you and only shows it once; there's no way to set your own. The confirmation dialog also lets you send a test event and create a compatible API key.

    To scope a webhook to one folder instead, open the folder → **Integrations** → **Webhooks** → **Create new webhook**, then follow the same steps. Granola automatically filters it to notes in that folder and its subfolders.
  </Tab>

  <Tab title="Granola API">
    Register your endpoint with Granola's create webhook endpoint API, through Nango's proxy:

    ```bash theme={null}
    curl -X POST "https://api.nango.dev/proxy/v1/webhook-endpoints" \
      -H "Authorization: Bearer <NANGO-API-KEY>" \
      -H "Provider-Config-Key: <INTEGRATION-ID>" \
      -H "Connection-Id: <CONNECTION-ID>" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "<NANGO-WEBHOOK-URL>?nangoConnectionId=<CONNECTION-ID>",
        "scopes": ["personal", "public"]
      }'
    ```

    Granola generates the `signing_secret` and returns it in the response — the request has no field to supply your own. It's returned only once; save it, you'll need it in the next step. Optionally pass `folder_ids` to scope the endpoint to specific folders, and `events` to subscribe to a subset (omit it to receive all events).

    The response also includes an `id` (e.g. `whe_2mKr8fQxLp7Ta3`) identifying this webhook endpoint — store it too, you'll need it to delete the endpoint later (see [step 4](#4-delete-the-webhook-endpoint-on-connection-deletion)).

    <Note>
      You can automate this with a [post-connection-creation script](/guides/functions/event-functions) so every new Granola connection registers its own webhook endpoint automatically:

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

      export default createOnEvent({
          event: 'post-connection-creation',
          description: 'Register a Granola webhook endpoint for this connection',
          metadata: z.object({
              webhookSecret: z.string().optional(),
              webhookEndpointId: z.string().optional()
          }),
          exec: async (nango) => {
              const webhookUrl = await nango.getWebhookURL();
              if (!webhookUrl) {
                  await nango.log('Skipping webhook endpoint registration: webhook URL is not configured', { level: 'error' });
                  return;
              }

              const config: ProxyConfiguration = {
                  endpoint: '/v1/webhook-endpoints',
                  data: {
                      url: `${webhookUrl}?nangoConnectionId=${nango.connectionId}`,
                      scopes: ['personal', 'public']
                  }
              };

              const response = await nango.post(config);

              // Store the secret for signature verification and the id for later deletion (step 4)
              await nango.updateMetadata({
                  webhookSecret: response.data.signing_secret,
                  webhookEndpointId: response.data.id
              });
          }
      });
      ```
    </Note>
  </Tab>
</Tabs>

### 3. Set the webhook secret in Nango

Granola generates a new, distinct `signing_secret` every time you create a webhook endpoint — you can't reuse one secret across multiple endpoints. Set each connection's `signing_secret` as `webhookSecret` in that connection's metadata — every connection needs its own, even if you only have one right now:

```bash theme={null}
curl -X POST "https://api.nango.dev/connections/metadata" \
  -H "Authorization: Bearer <NANGO-API-KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "connection_id": "<CONNECTION-ID>",
    "provider_config_key": "<INTEGRATION-ID>",
    "metadata": { "webhookSecret": "<SIGNING-SECRET>" }
  }'
```

See [Set connection metadata](/reference/backend/http-api/connections/set-metadata) for the full reference.

Granola doesn't let you retrieve the secret again after creation — if you lose it, create a new webhook endpoint and update both sides.

<Note>
  There's no integration-level webhook secret for Granola — Standard Webhooks signatures don't carry any binding to a specific destination, so a single secret shared across connections couldn't stop a delivery meant for one connection from being replayed against another. Each connection must have its own `webhookSecret` in its metadata; Nango rejects the delivery otherwise.
</Note>

### 4. Delete the webhook endpoint on connection deletion

If a connection is deleted in Nango but its Granola webhook endpoint remains active, Granola keeps sending deliveries to it — they'll just have no connection to route to. Delete the webhook endpoint before the connection is removed.

You can automate this with a `pre-connection-deletion` lifecycle event, using the `webhookEndpointId` stored in metadata during creation ([step 2](#2-register-the-webhook-in-granola)):

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

export default createOnEvent({
    event: 'pre-connection-deletion',
    description: "Delete the connection's Granola webhook endpoint before connection deletion",
    metadata: z.object({
        webhookEndpointId: z.string().optional()
    }),
    exec: async (nango) => {
        const metadata = await nango.getMetadata();
        if (!metadata.webhookEndpointId) {
            return;
        }

        const config: ProxyConfiguration = {
            endpoint: `/v1/webhook-endpoints/${metadata.webhookEndpointId}`
        };

        try {
            await nango.delete(config);
        } catch (err) {
            // Avoid blocking connection deletion if the delete call fails (e.g. already deleted upstream)
            await nango.log(`Failed to delete Granola webhook endpoint ${metadata.webhookEndpointId}: ${String(err)}`, { level: 'error' });
        }
    }
});
```

If you registered the webhook endpoint manually (the dashboard flow in [step 2](#2-register-the-webhook-in-granola) doesn't return an id to Nango), delete it from **Settings → Connectors → Webhooks** in Granola instead.

## Handle the webhook

Once routed, you have two options:

* **Forward it to your app** — Nango forwards the event to your webhook URL with connection attribution. See [External webhook forwarding](/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](/guides/functions/syncs/realtime-syncs).

## Supported events

| Event                 | Sent when                                                                      |
| --------------------- | ------------------------------------------------------------------------------ |
| `note.generated`      | The first AI summary for a note is generated while your endpoint can access it |
| `note.edited`         | The note's summary is edited or regenerated                                    |
| `note.access_granted` | A note is shared with you, directly or via a folder                            |

Subscribe to both `note.generated` and `note.access_granted` if you're using webhooks to discover notes — an already-generated note that's later shared with you triggers `note.access_granted`, not `note.generated`.

For the full payload schema, see Granola's webhooks documentation.

## Rollback strategy

To stop deliveries, delete the webhook endpoint using the `id` Granola returned when you created it:

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

Or delete it from **Settings → Connectors → Webhooks** in the Granola dashboard. Either way, also clear `webhookEndpointId` and `webhookSecret` from the connection's metadata so nothing references a deleted endpoint. Re-enable notifications by creating a new webhook endpoint with the steps above.

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