# Get a meeting transcript and recording

Source: https://developer.nylas.com/docs/cookbook/notetaker/get-transcript-and-recording/

Your bot sat through the meeting and recorded everything. Now you need the files: the transcript, the MP4 recording, the summary, and the action items. The catch is that the download links Nylas hands back expire after 3,600 seconds, so once you fetch them you have one hour to pull the bytes onto your own storage before the URLs stop working.

This recipe shows how to fetch the media for a finished meeting, what each object in the response holds, and how to download the files before the links go stale. If you haven't created a bot yet, a `POST /v3/grants/{grant_id}/notetakers` call sends one into a meeting. See the [Notetaker API guide](/docs/cookbook/notetaker/notetaker-api-guide/) for the full setup.

## Get the media for a meeting

Make a `GET` request to the media endpoint with the grant ID and the notetaker ID. Nylas returns one object per media type that was enabled for the meeting, each with a pre-authenticated `url` you can download without sending your API key again. The response carries five objects: `recording`, `transcript`, `summary`, `action_items`, and `thumbnail`.

```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/notetakers/<NOTETAKER_ID>/media" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'

```

```json
{
  "request_id": "5fa64c92-e840-4357-86b9-2aa364d35b88",
  "data": {
    "action_items": {
      "created_at": 1703088000,
      "expires_at": 1704297600,
      "name": "meeting_action_items.json",
      "size": 289,
      "ttl": 3600,
      "type": "application/json",
      "url": "https://storage.googleapis.com/nylas-notetaker-uc1-prod-notetaker/..."
    },
    "recording": {
      "created_at": 1703088000,
      "duration": 1800,
      "expires_at": 1704297600,
      "name": "meeting_recording.mp4",
      "size": 52428800,
      "ttl": 3600,
      "type": "video/mp4",
      "url": "https://storage.googleapis.com/nylas-notetaker-uc1-prod-notetaker/..."
    },
    "summary": {
      "created_at": 1703088000,
      "expires_at": 1704297600,
      "name": "meeting_summary.json",
      "size": 437,
      "ttl": 3600,
      "type": "application/json",
      "url": "https://storage.googleapis.com/nylas-notetaker-uc1-prod-notetaker/..."
    },
    "thumbnail": {
      "created_at": 1703088000,
      "expires_at": 1704297600,
      "name": "thumbnail.png",
      "size": 437,
      "ttl": 3600,
      "type": "image/png",
      "url": "https://storage.googleapis.com/nylas-notetaker-uc1-prod-notetaker/..."
    },
    "transcript": {
      "created_at": 1703088000,
      "expires_at": 1704297600,
      "name": "transcript.json",
      "size": 10240,
      "ttl": 3600,
      "type": "application/json",
      "url": "https://storage.googleapis.com/nylas-notetaker-uc1-prod-notetaker/..."
    }
  }
}


```

```js [getMedia-Node.js SDK]

import Nylas from "nylas";

const nylas = new Nylas({
  apiKey: "<NYLAS_API_KEY>",
  apiUri: "<NYLAS_API_URI>",
});

async function downloadMedia() {
  try {
    const media = await nylas.notetakers.downloadMedia({
      identifier: "<NYLAS_GRANT_ID>",
      notetakerId: "<NOTETAKER_ID>",
    });

    console.log("Media:", media);
  } catch (error) {
    console.error("Error downloading media:", error);
  }
}

downloadMedia();


```

```python [getMedia-Python SDK]

from nylas import Client

nylas = Client(
    "<NYLAS_API_KEY>",
    "<NYLAS_API_URI>",
)

media = nylas.notetakers.get_media(
    notetaker_id="<NOTETAKER_ID>",
    identifier="<NYLAS_GRANT_ID>",
)

print("Notetaker media:", media)


```

The endpoint is read-only and idempotent, so you can call it as often as you need. It only returns media once the notetaker reaches the `media_available` state. Call it too early and you'll get a `404`; call it after the retention window and you'll get a `410`.

## What's in the media response

The `data` object holds up to five media objects, and each one bundles a download `url` with metadata like `size`, `type`, and `ttl`. Which objects appear depends on the settings the bot used: a video meeting returns all five, while an audio-only run skips the thumbnail. The table below maps each object to its MIME type and contents.

| Object         | Type               | Contents                                                            |
| -------------- | ------------------ | ------------------------------------------------------------------- |
| `recording`    | `video/mp4`        | Full audio/video recording of the meeting.                          |
| `transcript`   | `application/json` | Speaker-labelled transcript with text segments and timestamps.      |
| `summary`      | `application/json` | Short text summary of what the meeting covered.                     |
| `action_items` | `application/json` | List of action items pulled from the conversation.                  |
| `thumbnail`    | `image/png`        | Still frame captured around the midpoint of the recording.          |

A couple of field details matter when you store this data. The `recording.duration` is the meeting length in seconds, so the sample's `1800` means a 30-minute call. The `recording.size` is in bytes, so `52428800` is a 50 MB file. Both help you decide whether to stream the download or buffer it in memory. For the full field list, see [Handling Notetaker media files](/docs/v3/notetaker/media-handling/).

## Download files before the links expire

Every media `url` carries a `ttl` of 3,600 seconds, which is the one-hour window you have to download the file before the link stops working. The fix is simple: as soon as you get the response, fetch each `url` and write the bytes to your own disk or object storage. The snippet below downloads the recording, and the same pattern works for the transcript, summary, and thumbnail.

```js [downloadRecording-Node.js]


// `media` is the response.data object from the Get Media call above.
const res = await fetch(media.recording.url);
if (!res.ok) throw new Error(`Download failed: ${res.status}`);

// Stream the MP4 straight to disk so large files don't sit in memory.
const file = fs.createWriteStream("meeting_recording.mp4");
await new Promise((resolve, reject) => {
  Readable.fromWeb(res.body).pipe(file).on("finish", resolve).on("error", reject);
});
console.log("Saved recording:", media.recording.size, "bytes");
```

```python
# `media` is the response data object from the Get Media call above.
# stream=True avoids loading the whole MP4 into memory.
with requests.get(media["recording"]["url"], stream=True) as res:
    res.raise_for_status()
    with open("meeting_recording.mp4", "wb") as f:
        for chunk in res.iter_content(chunk_size=8192):
            f.write(chunk)

print("Saved recording:", media["recording"]["size"], "bytes")
```

If an hour passes before you download, the link is dead. Don't try to "refresh" the same URL: call the Get Media endpoint again and Nylas mints a new set of URLs with a fresh 3,600-second window.

## Things to know about Notetaker media

Notetaker media has a handful of behaviors that shape how you build a reliable download pipeline. The points below cover link expiry, the transcript shape, language settings, how to know when files are ready, and where the recordings actually live.

### Links expire after 3,600 seconds

The `ttl` on each `url` is 3,600 seconds, and once it passes the link returns an error on any download attempt. This is a security constraint, not a bug: short-lived URLs limit exposure if one gets logged or shared by accident. Build your pipeline to re-fetch rather than cache URLs, because a stored link is worthless an hour later. The Get Media call is the only way to get fresh ones.

Separately, Nylas keeps the underlying files for a maximum of 14 days, tracked by the `expires_at` and `ttl` fields on each object. After that the files are deleted for good, so download anything you want to keep well inside that window.

### The transcript is speaker-labelled JSON

For most meetings the `transcript` file is a JSON object with `type: "speaker_labelled"`, a `language` code, and a `transcript` array. Each array entry has a `speaker` name, a `text` segment, and `start` and `end` times in milliseconds. So a single speaker's turn becomes one or more timed segments you can render as a caption track or search by timestamp.

In rare cases Nylas returns `type: "raw"` instead, where `transcript` is a plain string with no speakers or timing. Check the `type` field before you parse, because a transcript can come back raw and your handler should cover both shapes.

### Set expected languages up front

Notetaker auto-detects the spoken language and reports it in the transcript's `language` field. If your meetings run in a known set of languages, pass `transcription_settings.expected_languages` when you create the bot so the detected `language` usually matches one of your codes. This single setting noticeably improves accuracy on multilingual calls. See the [supported language codes](/docs/v3/notetaker/#supported-language-codes) for the full list.

### Poll or wait for the webhook

You have two ways to learn that media is ready. Polling means calling Get Media on a loop, but you'll burn requests and eat `404`s until processing finishes a few minutes after the bot leaves. The better path is the `notetaker.media` webhook, which fires once with `state: "available"` and includes the same download URLs. Wire that into your download step and you react in seconds instead of guessing. See the [Notetaker webhooks recipe](/docs/cookbook/notetaker/notetaker-webhooks/) for a full handler.

### Recordings are sensitive, so store them carefully

A meeting recording is some of the most sensitive data your app will touch. The pre-authenticated URLs work like bearer credentials: anyone holding one can download the file for 3,600 seconds with no further auth. Don't expose them in a frontend where they show up in browser network tabs, and proxy downloads through your backend instead. Once the files land on your infrastructure, put authentication, access control, and audit logging in front of them.

## What's next

- [Notetaker webhooks recipe](/docs/cookbook/notetaker/notetaker-webhooks/) to download media automatically when the `notetaker.media` event fires
- [Notetaker API guide](/docs/cookbook/notetaker/notetaker-api-guide/) for creating bots and every `meeting_settings` field
- [Transcribe a Zoom meeting](/docs/cookbook/notetaker/transcribe-zoom-meeting/) for a provider-specific walkthrough
- [Handling Notetaker media files](/docs/v3/notetaker/media-handling/) for the full media field reference, formats, and security model