Sections
MeetWave API · Guides
Get a Zoom meeting transcript with an API
Zoom's API has a transcript only with cloud recording and audio transcription on; for speakers and names, send the audio to MeetWave.
The short answer
Zoom's recordings API lists a TRANSCRIPT file, a VTT, only when the meeting was recorded to the cloud on a Pro or higher account with Create audio transcript turned on. When it is there, download it. When it is not, when it has "Unknown Speaker" lines, or the recording is local, take the recording's audio file, download it with your Zoom token on your side, and send it to MeetWave: you get JSON with speakers and the names said in the audio.
What Zoom's API gives you
GET https://api.zoom.us/v2/meetings/{meetingId}/recordings returns the meeting's recording_files. Each has a file_type and a download_url; download it with your OAuth token as Authorization: Bearer, and follow the redirect it may answer with. The app needs the scope cloud_recording:read:list_recording_files:admin (Zoom API reference, read 2026-10-21).
| file_type | Meaning |
|---|---|
TRANSCRIPT | Zoom's audio transcript, a VTT file. Only when "Create audio transcript" is on. |
M4A | The audio only. The file to send for a transcript with speakers and names. |
MP4 | The video. Works too when there is no M4A; it is larger to download and upload. |
CC | Closed captions, a VTT file. |
CHAT | The in-meeting chat, a TXT file. |
The TRANSCRIPT file exists only with cloud recording on a Pro, Business, Education or Enterprise account and the Create audio transcript setting on. Zoom puts each phrase under a participant, marks the ones it cannot place "Unknown Speaker" for you to rename in the web portal, and makes the transcript in English unless another of its 19 languages is chosen (Zoom support, read 2026-10-21).
What Zoom gives and what you get here
| Zoom TRANSCRIPT file | Recording audio through MeetWave | |
|---|---|---|
| Needs | Pro or higher, cloud recording, "Create audio transcript" on before the meeting | The recording's audio: the cloud M4A or MP4, or a local recording file |
| Speakers | Each phrase under the participant Zoom attributes it to, or "Unknown Speaker" | Each line has one speaker, with times for every word |
| Speaker names | Zoom's attribution; an unknown speaker is renamed by hand in the web portal | The name each voice is called in the audio, also for several people on one microphone |
| Languages | English by default; 19 languages listed by Zoom | 100 languages, detected or named |
| Format | VTT | JSON, TXT, SRT or VTT |
| Price per audio hour | Included in the Zoom plan | $0.12 text, $0.15 with speakers, $0.20 with names |
Zoom's transcript or the audio: one script for both
The script takes a meeting id and does both paths:
- Gets a Server-to-Server OAuth token:
POST https://zoom.us/oauth/tokenwithgrant_type=account_credentials, valid for one hour (Zoom). - Lists the recording's files.
- Zoom made a transcript: saves it as
zoom-transcript.vtt. - You need speakers and names: downloads the
M4A(or theMP4) with your Zoom token, uploads it to MeetWave and prints the transcript with speaker names.
Your Zoom token never leaves your machine: MeetWave gets the audio file, not access to your Zoom account. Already have the file, from the cloud or a local recording? Pass its path instead of the meeting id.
#!/usr/bin/env bash
# Turn a Zoom cloud recording into a transcript with speaker names.
#
# Zoom's own transcript (the TRANSCRIPT file) exists only when audio transcription
# is on; when Zoom made one, it is saved as zoom-transcript.vtt. The audio file
# (M4A, or the MP4 when there is no M4A) is downloaded here, with your Zoom token,
# then sent to MeetWave for a transcript with speakers and their names.
#
# export ZOOM_ACCOUNT_ID=... ZOOM_CLIENT_ID=... ZOOM_CLIENT_SECRET=... # Server-to-Server OAuth app
# export MEETWAVE_API_KEY=mwk_...
# bash zoom-recording.sh <meeting id>
# bash zoom-recording.sh zoom-audio.m4a # an audio file you already downloaded
#
# Needs curl and jq.
set -euo pipefail
API_BASE="${MEETWAVE_API_BASE:-https://api.meetwave.io}"
API_BASE="${API_BASE%/}"
if [ -z "${MEETWAVE_API_KEY:-}" ]; then
echo "Set MEETWAVE_API_KEY first. Get a key with one free hour: https://meetwave.io/speech-to-text-api?utm_source=github&utm_medium=integration&utm_campaign=api" >&2
exit 1
fi
if [ "$#" -ne 1 ]; then
echo "Usage: bash zoom-recording.sh <meeting id | audio file>" >&2
exit 1
fi
# What to do when the API refuses the key or the balance is empty.
hint() {
case "$1" in
401) echo "Check MEETWAVE_API_KEY, or get a key: https://meetwave.io/speech-to-text-api?utm_source=github&utm_medium=integration&utm_campaign=api" >&2 ;;
402) echo "Top up your balance: https://app.meetwave.io/developers?utm_source=github&utm_medium=integration&utm_campaign=api" >&2 ;;
esac
}
# Calls an API; prints the answer, or prints the error answer and stops.
call() {
local body status
body=$(curl -s -w '\n%{http_code}' "$@")
status="${body##*$'\n'}"
body="${body%$'\n'*}"
if [ "$status" -ge 400 ]; then
echo "$status $body" >&2
if [ -n "${MEETWAVE_CALL:-}" ]; then hint "$status"; fi
exit 1
fi
echo "$body"
}
meetwave() { MEETWAVE_CALL=1 call -A "meetwave-examples/zoom" -H "Authorization: Bearer $MEETWAVE_API_KEY" "$@"; }
if [ -f "$1" ]; then
AUDIO="$1"
else
# 1. A Server-to-Server OAuth token, valid for one hour; list the recording's files
ZOOM_TOKEN=$(call -X POST https://zoom.us/oauth/token -u "$ZOOM_CLIENT_ID:$ZOOM_CLIENT_SECRET" \
-d grant_type=account_credentials -d "account_id=$ZOOM_ACCOUNT_ID" | jq -r .access_token)
FILES=$(call "https://api.zoom.us/v2/meetings/$1/recordings" -H "Authorization: Bearer $ZOOM_TOKEN" | jq .recording_files)
# 2. Zoom made a transcript: save it as it is
TRANSCRIPT_URL=$(echo "$FILES" | jq -r 'map(select(.file_type == "TRANSCRIPT"))[0].download_url // empty')
if [ -n "$TRANSCRIPT_URL" ]; then
curl -sfL -H "Authorization: Bearer $ZOOM_TOKEN" -o zoom-transcript.vtt "$TRANSCRIPT_URL"
echo "Zoom's transcript saved to zoom-transcript.vtt"
fi
# 3. Download the audio on your side, with your Zoom token
FILE=$(echo "$FILES" | jq -c '(map(select(.file_type == "M4A")) + map(select(.file_type == "MP4")))[0] // empty')
[ -n "$FILE" ] || { echo "This recording has no M4A or MP4 file." >&2; exit 1; }
AUDIO="zoom-audio.$(echo "$FILE" | jq -r '.file_extension | ascii_downcase')"
curl -sfL -H "Authorization: Bearer $ZOOM_TOKEN" -o "$AUDIO" "$(echo "$FILE" | jq -r .download_url)"
fi
# 4. Upload it to MeetWave and transcribe it with speakers and their names
UPLOAD=$(meetwave -X POST "$API_BASE/v1/uploads")
curl -sf -T "$AUDIO" "$(echo "$UPLOAD" | jq -r .upload_url)" || { echo "Upload failed" >&2; exit 1; }
ID=$(meetwave "$API_BASE/v1/transcriptions" -H "Content-Type: application/json" \
-d "$(jq -n --arg id "$(echo "$UPLOAD" | jq -r .id)" '{upload_id: $id, speakers: true, speaker_names: true}')" | jq -r .id)
# 5. Wait while it is queued or processing, then print it
while :; do
TRANSCRIPTION=$(meetwave "$API_BASE/v1/transcriptions/$ID")
case "$(echo "$TRANSCRIPTION" | jq -r .status)" in
queued|processing) sleep 5 ;;
completed) break ;;
*) echo "Transcription failed: $(echo "$TRANSCRIPTION" | jq -c .error)" >&2; exit 1 ;;
esac
done
echo "$TRANSCRIPTION" | jq -r '.transcript.segments[]
| "[\(.start | floor / 60 | floor | tostring | if length < 2 then "0" + . else . end):\(.start | floor % 60 | tostring | if length < 2 then "0" + . else . end)] \(.speaker // "Speaker"): \(.text)"'
echo
echo "$TRANSCRIPTION" | jq -r '"\(.id): \(.billing.audio_seconds) s of audio, $\(.billing.cost_usd)"'
Each step of the MeetWave part is explained in Transcriptions.
Without polling: Zoom's webhook, then ours
- Subscribe your Zoom app to
recording.completed. Its body carries therecording_filesand adownload_tokenthat works for 24 hours (Zoom events, read 2026-10-21). - On that event, download the
M4Awith thedownload_tokenasAuthorization: Bearer. - Upload it with
POST /v1/uploadsand create the transcription withspeaker_names: trueand yourwebhook_url. - MeetWave sends
transcription.completedto that address; check its signature and fetch the transcript withGET /v1/transcriptions/{id}. See Webhooks.
Zoom also sends recording.transcript_completed when its own transcript is ready.
