# Get notified when a transcript is ready

Source: https://developer.nylas.com/docs/cookbook/notetaker/notetaker-webhooks/

After you send a Notetaker bot to a call, there's no clean signal for when the meeting actually ends or when the transcript finishes processing. Polling the media endpoint on a timer means you either check too often and waste requests, or check too rarely and make your users wait. Processing time also varies with meeting length, so a fixed interval never fits every call. Webhooks remove the guesswork: Nylas pushes a notification the instant a recording or transcript becomes available, and your handler reacts in one step.

This recipe wires up two Notetaker triggers so your app reacts to state changes and grabs media as soon as it's ready, instead of polling.

## Subscribe to Notetaker events

Two triggers cover the full lifecycle. The `notetaker.meeting_state` event reports state changes as the bot joins, records, and leaves, and `notetaker.media` fires once the recording and transcript finish processing and are ready to download. Subscribing to both means you track the meeting live and still get a clean "files are ready" signal at the end.

Create the webhook with one `POST /v3/webhooks/` request. Pass your HTTPS endpoint as `webhook_url`, the two triggers in `trigger_types`, and an optional `notification_email_addresses` list that Nylas alerts if delivery starts failing. The endpoint must return `200 OK` within 10 seconds.

```bash
curl --request POST \
  --url 'https://api.us.nylas.com/v3/webhooks/' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --data-raw '{
    "trigger_types": ["notetaker.media", "notetaker.meeting_state"],
    "webhook_url": "https://yourapp.com/webhooks/nylas",
    "description": "Notetaker transcript-ready notifications",
    "notification_email_addresses": ["alerts@yourcompany.com"]
  }'
```

```js [createNotetakerWebhook-Node.js SDK]
const webhook = await nylas.webhooks.create({
  requestBody: {
    triggerTypes: ["notetaker.media", "notetaker.meeting_state"],
    webhookUrl: "https://yourapp.com/webhooks/nylas",
    description: "Notetaker transcript-ready notifications",
    notificationEmailAddresses: ["alerts@yourcompany.com"],
  },
});
```

```python [createNotetakerWebhook-Python SDK]
webhook = nylas.webhooks.create(
    request_body={
        "trigger_types": ["notetaker.media", "notetaker.meeting_state"],
        "webhook_url": "https://yourapp.com/webhooks/nylas",
        "description": "Notetaker transcript-ready notifications",
        "notification_email_addresses": ["alerts@yourcompany.com"],
    }
)
```

When you create the webhook, the API sends a `GET` request with a `challenge` query parameter that your endpoint echoes back to confirm the URL. For the full create flow and the challenge handshake, see [using webhooks with Nylas](/docs/v3/notifications/).

## What the notetaker.media payload contains

The `notetaker.media` event arrives when processing finishes, with `status` and `state` set to `available`. The payload's `data.object` carries the bot identifiers plus a `media` object holding signed download URLs. You don't get the file bytes in the webhook; you get the IDs you need to fetch them. Across all Notetaker events, the payload shape stays the same except for the inner state and `media` fields.

The two IDs you extract are `data.object.id` (the Notetaker ID) and `data.object.grant_id`. Together they build the media endpoint URL in the next step. The `media` object also reports `recording_duration` in seconds and `recording_file_format`, so your handler can branch on file type before downloading.

```json
{
  "specversion": "1.0",
  "type": "notetaker.media",
  "source": "/nylas/notetaker",
  "id": "<WEBHOOK_ID>",
  "time": 1737500935555,
  "data": {
    "application_id": "<NYLAS_APPLICATION_ID>",
    "object": {
      "id": "<NOTETAKER_ID>",
      "grant_id": "<NYLAS_GRANT_ID>",
      "object": "notetaker",
      "status": "available",
      "state": "available",
      "media": {
        "recording": "<SIGNED_URL>",
        "recording_duration": "1800",
        "recording_file_format": "mp4",
        "thumbnail": "<SIGNED_URL>",
        "transcript": "<SIGNED_URL>",
        "summary": "<SIGNED_URL>",
        "action_items": "<SIGNED_URL>"
      }
    }
  }
}
```

## React to a ready transcript

When the `notetaker.media` event lands, call [`GET /v3/grants/<NYLAS_GRANT_ID>/notetakers/<NOTETAKER_ID>/media`](/docs/v3/notetaker/media-handling/) using the two IDs from the payload to get fresh signed URLs for the recording, transcript, summary, and action items. The signed links the endpoint returns expire after 3,600 seconds (one hour), so fetch and store the files right away rather than queuing the work for later.

The handler below verifies the trigger type, pulls the IDs, and requests the media. Return `200 OK` fast and move the download to a background job if it's slow, since Nylas treats anything past the 10-second window as a failed delivery.

```js [readyTranscriptHandler-Node.js SDK]
app.post("/webhooks/nylas", async (req, res) => {
  const { type, data } = req.body;
  res.sendStatus(200); // ack within 10 seconds

  if (type === "notetaker.media") {
    const media = await nylas.notetakers.downloadMedia({
      identifier: data.object.grant_id,
      notetakerId: data.object.id,
    });
    await storeTranscript(media); // links expire after 3,600 seconds
  }
});
```

```python [readyTranscriptHandler-Python SDK]
@app.post("/webhooks/nylas")
def nylas_webhook(payload: dict):
    if payload["type"] == "notetaker.media":
        obj = payload["data"]["object"]
        media = nylas.notetakers.get_media(
            identifier=obj["grant_id"],
            notetaker_id=obj["id"],
        )
        store_transcript(media)  # links expire after 3,600 seconds
    return "", 200
```

## Things to know about Notetaker webhooks

These details decide whether your handler stays reliable in production. Each one maps to a real field in the `notetaker.meeting_state` payload or a delivery behavior shared by every Nylas webhook. Plan for all four before you ship; ordering and retries cause most of the surprises.

### Meeting state values

The `notetaker.meeting_state` payload carries both a top-level `state` and a more specific `meeting_state` field. When `state` is `connecting`, `meeting_state` is `waiting_for_entry`. When `state` is `attending`, it's `recording_active`, the signal that recording started. When `state` is `failed_entry`, `meeting_state` is one of 12 error reasons: `error`, `internal_error`, `bad_meeting_code`, `bad_meeting_link`, `sign_in_required`, `entry_denied`, `no_response`, `cannot_join`, `meeting_capacity_reached`, `admission_timeout`, `network_error`, or `cancelled`. Branch on these to retry, alert, or re-invite the bot.

### Verify the signature

Every notification carries an `X-Nylas-Signature` header: a hex-encoded HMAC-SHA256 of the raw request body, signed with your `webhook_secret`. Recompute the HMAC over the unmodified body and reject any request that doesn't match before you act on it. See [secure a webhook](/docs/v3/notifications/#secure-a-webhook) for the verification details and where the `webhook_secret` comes from, and [webhook best practices](/docs/dev-guide/best-practices/webhook-best-practices/) for retries and compressed delivery.

### Retries and idempotency

Nylas retries delivery when your endpoint doesn't return `200 OK`, so the same `notetaker.media` event can arrive more than once. Each payload includes a `webhook_delivery_attempt` counter and a stable top-level `id`. Key your processing on that `id` (or on the Notetaker ID) so a duplicate delivery doesn't trigger a second download or a duplicate database row.

### Ordering isn't guaranteed

Webhooks aren't guaranteed to arrive in the order events happened, so a later `meeting_state` change can land before an earlier one. Trust the `time` field on each payload rather than arrival order, and treat `notetaker.media` as the authoritative "ready" signal instead of inferring readiness from a sequence of state events.

## What's next

- [Get the transcript and recording](/docs/cookbook/notetaker/get-transcript-and-recording/) to download finished files once the media event fires
- [Notetaker API guide](/docs/cookbook/notetaker/notetaker-api-guide/) for the full bot lifecycle
- [Using webhooks with Nylas](/docs/v3/notifications/) for setup, the challenge handshake, and failure handling
- [Using Nylas Notetaker](/docs/v3/notetaker/) for meeting settings, summaries, and media handling