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

# Webhooks

> Granola webhooks documentation. Receive HTTP notifications when meeting notes change, verify delivery signatures, and handle retries.

Webhooks notify your systems when notes change in Granola, so you don't have to poll the API. You register an HTTPS URL as a webhook endpoint; when a subscribed event occurs, Granola sends it an HTTP POST with a small JSON payload, and you fetch the latest data through the [Granola API](/introduction).

Webhooks are available on Business and Enterprise plans.

## Set up a webhook endpoint

You'll need a [Granola API key](/help-center/sharing/integrations/granola-api). A webhook endpoint uses the same access scopes as API keys; the scopes control which notes it receives events for:

| Scope      | Events you'll receive                                                                       |
| ---------- | ------------------------------------------------------------------------------------------- |
| `personal` | Notes you own, notes shared directly with you, and notes in private folders shared with you |
| `public`   | Notes visible to everyone in the workspace                                                  |

Register your endpoint with [Create webhook endpoint](/api-reference/create-webhook-endpoint):

```bash theme={null}
$ curl -X POST "https://public-api.granola.ai/v1/webhook-endpoints" \
  -H "Authorization: Bearer grn_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/granola-webhooks",
    "scopes": ["personal", "public"]
  }'
{
  "id": "whe_2mKr8fQxLp7Ta3",
  "object": "webhook_endpoint",
  "url": "https://example.com/granola-webhooks",
  "events": ["note.access_granted", "note.edited", "note.generated", "note.regenerated"],
  "scopes": ["personal", "public"],
  "created_by": {
    "name": "Oat Benson",
    "email": "oat@granola.ai"
  },
  "enabled": true,
  "created_at": "2026-01-27T15:30:00Z",
  "signing_secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
```

<Note>
  The `signing_secret` is returned only in this response and cannot be
  retrieved later. Store it securely. You need it to verify that deliveries
  came from Granola.
</Note>

The URL must be HTTPS and publicly reachable; addresses on private networks are rejected. `events` chooses which [events](#events) the endpoint receives; omit it to subscribe to all events.

On Enterprise plans, workspace admins control which scopes members can use in **Settings → Workspace → General → API access for members**, the same controls that govern API keys.

## Events

Each way a note can change has its own event. Your endpoint receives the events it subscribed to, for notes in its scope:

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

`note.generated` is sent only when a note's first summary is generated while your endpoint can access it. If an already-generated note is later shared with you, your endpoint receives `note.access_granted` instead. Subscribe to both events when using webhooks to discover notes.

An example payload:

```json theme={null}
{
  "event_id": "8f1c2a4e-6b3d-4e8f-9a2b-1c5d7e9f0a3b",
  "event_type": "note.generated",
  "note_id": "not_1d3tmYTlCICgjy",
  "occurred_at": "2026-01-27T15:30:00Z"
}
```

| Field         | Description                                                                     |
| ------------- | ------------------------------------------------------------------------------- |
| `event_id`    | Unique ID for the event. Retries of the same delivery reuse it.                 |
| `event_type`  | Which event occurred.                                                           |
| `note_id`     | The note the event is about. Fetch it with [Get Note](/api-reference/get-note). |
| `occurred_at` | When the event occurred (ISO 8601).                                             |

`note.edited` payloads also include a `data` object with `changed_fields`, the note fields that changed. Currently this is always `["summary"]`.

Payloads carry no note content. When you receive one, fetch the note with your API key. Access checks apply at fetch time, so a delivery never exposes more than the API would.

## Verify deliveries

Every delivery is signed following the [Standard Webhooks](https://www.standardwebhooks.com/) specification, so you can verify it with any Standard Webhooks library, or a few lines of code.

| Header              | Description                                          |
| ------------------- | ---------------------------------------------------- |
| `webhook-id`        | The event ID (matches `event_id` in the payload)     |
| `webhook-timestamp` | Unix timestamp, in seconds, of this delivery attempt |
| `webhook-signature` | `v1,` followed by a base64 HMAC-SHA256 signature     |

The signature is an HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{body}`, keyed with your signing secret (base64-decoded, after the `whsec_` prefix).

Sample code for verifying it yourself:

```typescript theme={null}
import crypto from "crypto";

function verifyGranolaSignature(
  headers: Record<string, string>,
  rawBody: string,
  signingSecret: string
): boolean {
  const key = Buffer.from(signingSecret.slice("whsec_".length), "base64");
  const signedContent = `${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${rawBody}`;
  const expected = Buffer.from(
    crypto
      .createHmac("sha256", key)
      .update(signedContent, "utf8")
      .digest("base64")
  );

  return (headers["webhook-signature"] ?? "").split(" ").some((versioned) => {
    const [version, signature = ""] = versioned.split(",");
    const provided = Buffer.from(signature);
    return (
      version === "v1" &&
      provided.length === expected.length &&
      crypto.timingSafeEqual(provided, expected)
    );
  });
}
```

Compute the signature over the raw request body and parse the JSON only after verifying. To protect against replayed deliveries, reject requests whose `webhook-timestamp` is more than a few minutes old.

## Deliveries and retries

Your endpoint has 15 seconds to respond, so acknowledge with a `2xx` before any heavy processing.

| Response                                      | Result                                         |
| --------------------------------------------- | ---------------------------------------------- |
| `2xx`                                         | Delivered                                      |
| `408`, `429`, `5xx`, network errors, timeouts | Retried                                        |
| `3xx`                                         | Failed permanently; redirects are not followed |
| Other `4xx`                                   | Failed permanently                             |

Failed webhook deliveries are retried using exponential backoff over a 24-hour period. A slow or failed response can trigger a retry for an event you already processed, so deduplicate on `event_id` if handling an event twice would cause problems.

## Manage webhook endpoints

List the endpoints your key can manage. A personal key sees the endpoints you created; a workspace key (or a workspace admin on Enterprise) sees every endpoint in the workspace:

```bash theme={null}
$ curl "https://public-api.granola.ai/v1/webhook-endpoints" \
  -H "Authorization: Bearer grn_YOUR_API_KEY"
{
  "webhook_endpoints": [
    {
      "id": "whe_2mKr8fQxLp7Ta3",
      "object": "webhook_endpoint",
      "url": "https://example.com/granola-webhooks",
      "events": ["note.generated"],
      "scopes": ["personal"],
      "created_by": {
        "name": "Oat Benson",
        "email": "oat@granola.ai"
      },
      "enabled": true,
      "created_at": "2026-01-27T15:30:00Z"
    }
  ]
}
```

<Note>
  The signing secret is never included in list responses; it's shown once, on
  creation.
</Note>

Delete an endpoint to stop its deliveries immediately:

```bash theme={null}
$ curl -X DELETE "https://public-api.granola.ai/v1/webhook-endpoints/whe_2mKr8fQxLp7Ta3" \
  -H "Authorization: Bearer grn_YOUR_API_KEY"
{
  "id": "whe_2mKr8fQxLp7Ta3",
  "object": "webhook_endpoint",
  "deleted": true
}
```

See the API reference for full request and response schemas: [Create webhook endpoint](/api-reference/create-webhook-endpoint), [List webhook endpoints](/api-reference/list-webhook-endpoints), [Delete webhook endpoint](/api-reference/delete-webhook-endpoint).
