Finding the value for download_access_token for the Get meeting recordings API endpoint

I am using the Get Meeting Recording as referenced here: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/recordingGet.

The article mentions a download_access_token querystring parameter but makes no mention of how to get the value for this parameter. There is a sample value in the documentation but it’s not a JWT token. This documentation is dangerous and people will paste their access tokens into querystrings and that’s a terrible idea.

I understand that I can use an access token in Authorization to get this but that requires some overhead that I’d like to avoid.

The download_url property returned in the response of the method above does have something appended to the URL that somewhat looks like a JWT token (it’s not) but if I paste that value into the browser, it asks for a passcode which does not help me programmatically.

I’ve engaged Zoom support because the documentation is neither clear nor complete and they looked at the documentation and told me to come here. Clearly this is a Zoom problem and not a community problem but here I am.

What’s interesting is, I also have a webhook that handles the recording completed event. Until today, that webhook included a token in the request that I could use. Suddenly, that token is gone. I’ve added every scope I can think to add to see if I can get it back. Zoom seems to be making breaking changes and not communicating it very well. It would be nice to see them version their APIs if they are going to do that.

@jheavner Hope you will be fine.

You have two ways to download Zoom cloud recording

  • using download_access_token you will get that from recording.completed Webhook payload

and set that as query param.

  • using access_token you will get that after OAuth 2.0 authorization code exchange.

Let me know if any query.

Thanks

@jheavner Here are the sessions

Zoom OAuth2.0 Part - 1
Zoom OAuth2.0 Part - 2

Zoom Cloud Recording Downloader Automation

You should update the documentation for this endpoint: Zoom Meeting API.

As of today, download_access_token is no longer part of the recording.completed payload. It was there a few days ago for sure but it’s not there today. Here’s what Zoom is passing me, note that download_access_token is gone. Sensitive data has been removed.

{
“account_id”: “_KgVNjYg”,
“object”: {
“uuid”: “FevAdfFPEg==”,
“id”: 84372,
“account_id”: “_KgyNjYg”,
“host_id”: “jtbfaUFeqg”,
“topic”: “Jay’s Zoom Meeting”,
“type”: 1,
“start_time”: “2024-05-10T16:20:49Z”,
“timezone”: “”,
“host_email”: “”,
“duration”: 0,
“total_size”: 1047882,
“recording_count”: 3,
“share_url”: “Error - Zoom”,
“recording_files”: [
{
“id”: “82656c0ebf1c6d”,
“meeting_id”: “Fev61KfFPEg==”,
“recording_start”: “2024-05-10T16:20:58Z”,
“recording_end”: “2024-05-10T16:21:29Z”,
“file_type”: “TRANSCRIPT”,
“file_extension”: “VTT”,
“file_size”: 449,
“play_url”: “Error - Zoom”,
“download_url”: “https://www.zoom.us/rec/webhook_download/SPD_88bHQK1oxPN9NbvDTCSOnNqxdH4pJk-PgdaY.BSdXKUkwiRzphpSZ”,
“status”: “completed”,
“recording_type”: “audio_transcript”
}
],
“password”: “YQNs”,
“recording_play_passcode”: “eIYHOgbhFCBcH”
}
}

Also, I’m using this in Lambda. Having to manage access token refresh properly is a lot more work than simply grabbing the value from the payload and using it directly.

It is download_token as below

{
  "event": "recording.completed",
  "event_ts": 1626230691572,
  "payload": {
    "account_id": "AAAAAABBBB",
    "object": {
      "id": 1234567890,
      "uuid": "4444AAAiAAAAAiAiAiiAii==",
      "host_id": "x1yCzABCDEfg23HiJKl4mN",
      "account_id": "x1yCzABCDEfg23HiJKl4mN",
      "topic": "My Personal Recording",
      "type": 4,
      "start_time": "2021-07-13T21:44:51Z",
      "password": "132456",
      "timezone": "America/Los_Angeles",
      "host_email": "jchill@example.com",
      "duration": 60,
      "share_url": "https://example.com",
      "total_size": 3328371,
      "recording_count": 2,
      "on_prem": false,
      "recording_play_passcode": "yNYIS408EJygs7rE5vVsJwXIz4-VW7MH",
      "recording_files": [
        {
          "id": "ed6c2f27-2ae7-42f4-b3d0-835b493e4fa8",
          "meeting_id": "098765ABCD",
          "recording_start": "2021-03-23T22:14:57Z",
          "recording_end": "2021-03-23T23:15:41Z",
          "recording_type": "audio_only",
          "file_type": "M4A",
          "file_size": 246560,
          "file_extension": "M4A",
          "play_url": "https://example.com/recording/play/Qg75t7xZBtEbAkjdlgbfdngBBBB",
          "download_url": "https://example.com/recording/download/Qg75t7xZBtEbAkjdlgbfdngBBBB",
          "status": "completed"
        },
        {
          "id": "388ffb46-1541-460d-8447-4624451a1db7",
          "meeting_id": "098765ABCD",
          "recording_start": "2021-03-23T22:14:57Z",
          "recording_end": "2021-03-23T23:15:41Z",
          "recording_type": "shared_screen_with_speaker_view",
          "file_type": "MP4",
          "file_size": 282825,
          "file_extension": "MP4",
          "play_url": "https://example.com/recording/play/Qg75t7xZBtEbAkjdlgbfdngCCCC",
          "download_url": "https://example.com/recording/download/Qg75t7xZBtEbAkjdlgbfdngCCCC",
          "status": "completed"
        }
      ],
      "participant_audio_files": [
        {
          "id": "ed6c2f27-2ae7-42f4-b3d0-835b493e4fa8",
          "recording_start": "2021-03-23T22:14:57Z",
          "recording_end": "2021-03-23T23:15:41Z",
          "file_type": "M4A",
          "file_name": "MyRecording",
          "file_size": 246560,
          "file_extension": "MP4",
          "play_url": "https://example.com/recording/play/Qg75t7xZBtEbAkjdlgbfdngAAAA",
          "download_url": "https://example.com/recording/download/Qg75t7xZBtEbAkjdlgbfdngAAAA",
          "status": "completed"
        }
      ]
    }
  },
  "download_token": "abJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJodHRwczovL2V2ZW50Lnpvb20udXMiLCJhY2NvdW50SWQiOiJNdDZzdjR1MFRBeVBrd2dzTDJseGlBIiwiYXVkIjoiaHR0cHM6Ly9vYXV0aC56b29tLnVzIiwibWlkIjoieFp3SEc0c3BRU2VuekdZWG16dnpiUT09IiwiZXhwIjoxNjI2MTM5NTA3LCJ1c2VySWQiOiJEWUhyZHBqclMzdWFPZjdkUGtrZzh3In0.a6KetiC6BlkDhf1dP4KBGUE1bb2brMeraoD45yhFx0eSSSTFdkHQnsKmlJQ-hdo9Zy-4vQw3rOxlyoHv583JyZ"
}

If not present there then you need to use access token as authorization header and download.

Your object is from 2021. I’m saying the property is no longer present but it was just a few days ago. That’s a breaking change to your API. Saying if it’s not present then I need to use an access token is a terrible answer.

I just got another payload and it also does not have the property. The behavior is consistent for me. I also added every recording scope to see if that changed the behavior. It did not.

Think of it this way, what’s the point of providing a download_url in a webhook payload if I need to then make a call back to your API to download a file. That doesn’t make sense.

It’s worse than I thought. The download URL value returned by the webhook has a URL pattern of: https://xxx.zoom.us/rec/webhook_download/_string_._more_string_/_even_more_string_.shorter_string_

The download URL value returned by the API has a pattern of: https://xxx.zoom.us/rec/download/_string_._shorter_string_

I don’t know why different URLs would be returned. I can pass an access token as an auth header to the second pattern and it works. If I try on the first pattern I get this error:

{
    "status": false,
    "errorCode": 300,
    "errorMessage": "Forbidden"
}

What kind of response is that? It’s not a standard one.

But wait, there’s more!

If I take the payload I get from the webhook, which looks like this:
https://xxx.zoom.us/rec/webhook_download/_string_._more_string_/_even_more_string_.shorter_string_

and I remove both the “webhook_” and everything after the “/” then it looks like the URL from the API call. AND IT WORKS.

Seriously, what is Zoom doing over there? I don’t really want to write code to handle this because it has to be wrong.

The article mentions a download_access_token querystring parameter but makes no mention of how to get the value for this parameter.

This is a bug in the doc. I’ll fix it.

You need to include download_access_token in the include_fields query, not the value of the token. For example, the endpoint syntax should be something like this:
https://api.zoom.us/v2/meetings/{meeting_ID}/recordings?include_fields=download_access_token

Thanks for discovering this bug and I am sorry for any confusion.

The download URL value returned by the webhook

Which webhook are you referring to? Thanks.

Hi, I have been having the same issue for the last two years for the recording complete and transcript complete endpoints. I am receiving the download_token and multiple download_url for each recording, and using this function to download them:

public async Task DownloadZoomFile(string token, string filename, string filedir, string download_url)
{
    string localFullFilename = Path.Combine(filedir, filename);

    var client = new RestClient(download_url);
    var request = new RestRequest();
    request.AddHeader("Authorization", "Bearer " + token);

    DirectoryInfo dir = new DirectoryInfo(filedir);
    if (!dir.Exists)
    {
        dir.Create();
    }

    using Stream writer = System.IO.File.OpenWrite(localFullFilename);
    using Stream? reader = await client.DownloadStreamAsync(request);

    if(reader == null)
    {
        // Execute request again as a test, shouldn't throw errors
        var test_response = await client.ExecuteAsync(request);

        throw new Exception("Could not read from stream."
                            + "\nRequest URL: " + download_url
                            + "\nRequest token: " + token
                            + "\nResponse status code: " + test_response.StatusCode
                            + "\nResponse content: " + test_response?.Content
                            + "\nResponse error message: " + test_response?.ErrorMessage);
    }

    await reader.CopyToAsync(writer);
}

This function has around a 90% chance of downloading zoom files successfully, but 10% of the time, I record an error like this:

Response status code: Unauthorized
Response content: {"status":false,"errorCode":300,"errorMessage":"Forbidden"}

Looking at the download_token, this should not be invalid, since the JWT exp and iat values are:

"exp": 1720923045 (Jul 13 20:10:45),
"iat": 1720836645 (Jul 12 20:10:45),

and the logs show the download attempt at 2024-07-12 20:10:46.394.

There have been multiple forum threads on this issue going back to 2020 saying resolved, and the issue is still going:

/t/downloading-cloud-recordings-forbidden/86522
/t/bug-downloading-cloud-recordings-with-access-token-set-results-in-an-invalid-response/4292/60

We have a large json list of failed downloads that someone will need to go through manually, and not a lot of available employee hours.

Help??