@esauflores/zoom-dl
v1.0.1
Published
Download Zoom cloud recordings — including passcode-protected ones and ones with the download button disabled
Readme
zoom-dl
Download Zoom cloud recordings — including passcode-protected ones and ones where the owner disabled the download button.
npm i -g @esauflores/zoom-dl
zoom-dl <url> [passcode] [seconds] [-o dir]
zoom-dl 'https://us02web.zoom.us/rec/play/...' 'KD+ZLT1s' # full recording
zoom-dl 'https://us02web.zoom.us/rec/share/...' '*8q*n4mW' 10 # 10s preview
zoom-dl 'https://us02web.zoom.us/rec/component-page?...' '...' -o /tmpAll three URL forms work: /rec/play/..., /rec/share/..., and the passcode page
(/rec/component-page?..., unwrapped via its originRequestUrl param). The ?iet=...
token in the link is what matters — it's the share session token.
Runs on Node.js >= 18. From a checkout instead of npm: bun run zoom-dl ....
The seconds preview trims with ffmpeg-static; if its binary is missing (install
scripts blocked), it falls back to a system ffmpeg. Output goes to
~/Downloads (-o dir or ZOOM_DL_DIR to change), named after the meeting topic.
how it works
Zoom's new recording player is an SPA, and none of the community tools (yt-dlp, zoomdl,
zoom-dl) handle it — they still target the legacy password_form page and the dead
/rec/validate_meet_passwd endpoint. This is the current flow, reverse-engineered from
the player bundles:
url ──► play page ──► fileId ──► play/info ──► need-password? ──► validate ──► play/info
│ │ (passcode)
└── share links: play/share-info/{meetingId} ──► play page
(may itself be the gate)
play/info ──► viewMp4Url ──► playcheck ──► got stream (cookies + Referer) ──► .mp4Session + CSRF. Every run gets a cookie jar, then a CSRF token:
POST /csrf_js?t_x_zm_rid=1with headerFETCH-CSRF-TOKEN: 1returnsname:value(OWASP CSRFGuard), sent back as a header on later POSTs.fileId. The play page embeds
fileIdinwindow.__data__. Share links don't have one:GET /nws/recording/1.0/play/share-info/{meetingId}answers with a redirect to the play page — or with the passcode gate (step 3).Passcode gate.
play/info(orshare-info) answersresult.componentName = "need-password"plus aredirectUrlpointing at the passcode component page. From there:POST /nws/recording/1.0/validate-contextwithmeetingId(from the component page'swindow.__data__),fileId,useWhichPasswd,sharelevel,iet→ returnsencryptMeetIdPOST /nws/recording/1.0/validate-meeting-passwdwithid=encryptMeetId,passwd=<passcode>,action=viewdetailpage(orvalidate-passwdwith the fileId for file-level passcodes) → sets the session cookie
Media URL.
play/inforeturnsviewMp4Url— a CloudFront-signed mp4 URL that is served even whendisableDownload: true. The download button is just UI.CDN unlock.
GET /nws/recording/1.0/playcheck/{playId}?accid=...&dur=0first — a Lambda@Edge onssrweb.zoom.us403s (Forbbiden, sic) every media request until the session is playchecked.Transfer.
gotstreams to disk over the sharedtough-cookiejar withReferer: https://<host>/— non-2xx throws, so error pages never end up saved as.mp4. The seconds mode requests a byte range (2x the average byte rate + moov headroom — the mp4 hasmoovup front) and trims locally with the bundledffmpeg -t N -c copy. Full downloads resume partial files with a Range request (append only when the server answers 206); filenames carry the full recording id, so a resume can only ever resume the same recording, and a finished file is skipped on re-runs.
development
bun install
bun run check # oxlint + oxfmt --check + tsc --noEmit
bun run check:fix # oxlint --fix + oxfmt --write
bun run test # vitestsrc/zoom-dl.ts CLI + core flow (run + commander)
src/helpers/ helpers by category:
errors.ts die/errMsg
http.ts fetchPage/fetchJson/transfer + isHttp
parse.ts url / page / arg parsing
media.ts response → identity + output naming
download.ts preview + resume math
src/types.ts response contracts (only the fields we read)
src/zoom-dl.test.ts vitest specs for the pure helpersPure helpers (parseUrl, resolveStart, pageVal, gateOf, mediaMeta, slug,
outName, previewBytes, parseSeconds) are unit-tested; the networked flow is proven by real downloads —
all three URL forms above pass end-to-end.
limits
- multi-clip recordings (
totalClips > 1) pull the first clip only - no transcript / chapter export (play/info has the VTT URLs if we ever want it)
