Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⚠️ The following sample application is a personal, open-source project shared by the app creator and not an officially supported Zoom Communications, Inc. sample application. Zoom Communications, Inc., its employees and affiliates are not responsible for the use and maintenance of this application. Please use this sample application for inspiration, exploration and experimentation at your own risk and enjoyment. You may reach out to the app creator and broader Zoom Developer community on https://devforum.zoom.us/ for technical discussion and assistance, but understand there is no service level agreement support for this application. Thank you and happy coding!

⚠️ このサンプルのアプリケーションは、Zoom Communications, Inc.の公式にサポートされているものではなく、アプリ作成者が個人的に公開しているオープンソースプロジェクトです。Zoom Communications, Inc.とその従業員、および関連会社は、本アプリケーションの使用や保守について責任を負いません。このサンプルアプリケーションは、あくまでもインスピレーション、探求、実験のためのものとして、ご自身の責任と楽しみの範囲でご活用ください。技術的な議論やサポートが必要な場合は、アプリ作成者やZoom開発者コミュニティ( https://devforum.zoom.us/ )にご連絡いただけますが、このアプリケーションにはサービスレベル契約に基づくサポートがないことをご理解ください。

Zoom My Notes — transcript PoC

Pull the transcript of an in-person (face-to-face) recording out of Zoom My Notes through the REST API.

Zoom's in-person recording drops a note into the My Notes preset folder of Zoom Canvas, and the transcript of that session is bound to the note. So the whole job is: find the note id, then ask the My Notes API for the content with include=transcript.

Two discovery paths are implemented:

# How Endpoints
A Walk the Canvas folder tree GET /docs/files/root/children → find the My Notes folder → GET /docs/files/{folderId}/children
B React to an event my_notes.note_generated via Webhook or WebSocket

GET /my_notes/notes is not a third path. It requires a meetingId, and an in-person recording has no meeting — so there is no value to pass. Note listing for this use case has to go through Canvas.

Both end at the same call:

GET /my_notes/notes/{noteId}/content?include=transcript

Zero runtime dependencies — Node 22 built-ins only (fetch, node:http, global WebSocket, --env-file).

Layout

src/
  auth.js               # User OAuth: authorize → code → access/refresh token
  list-notes.js         # A: discover note ids (and print the Canvas tree)
  get-transcript.js     # GET /my_notes/notes/{noteId}/content?include=transcript
  webhook-server.js     # B: HTTP receiver (url_validation + signature check)
  websocket-client.js   # B: WebSocket receiver (no public URL required)
  lib/
    config.js           # env plumbing + the scope list
    oauth.js            # credential cache with auto refresh
    api.js              # bearer token, query building, 429 retry
    notes.js            # Canvas walk, note listing, content fetch
    events.js           # note_generated handling + webhook crypto
    render.js           # Markdown / plain text / WebVTT output

Setup

1. Create a User-managed OAuth app

Zoom App Marketplace → Develop → Build App → General App, app type User-managed.

  • Redirect URL for OAuth: http://localhost:4000/callback

  • Add the same URL to the OAuth Allow List.

  • Scopes (Classic scopes are deprecated — request the granular form only):

    Purpose Scope
    Note content + transcript my_notes:read:content
    Canvas folder walk docs:read:list_children

Every scope above is user-level. There is no account-level (:admin) variant for My Notes today, so Server-to-Server OAuth cannot read another user's notes — each user has to authorise the app individually.

2. Configure

cp .env.example .env
$EDITOR .env          # ZOOM_CLIENT_ID / ZOOM_CLIENT_SECRET at minimum

3. Authorise

npm run auth

Open the printed URL as the target user, approve, and the access/refresh pair lands in .tokens.json (git-ignored, mode 600).

Usage

Discover note ids

npm run list                # note ids in the My Notes folder
npm run list -- --tree      # dump the folder tree under "root"
Canvas walk — folder "My Notes" (Kx9dQ2VeRb6McD1sFgHy8A):
  Nt4kPzQwRb6McD1sFgHy8A  2026-08-20T05:12:03Z  In-person sync with the SI team
  lXyU1N4sTzSON6cgVwQ48Q  2026-08-18T01:44:11Z  Weekly Sync Notes

root is the documented alias for the current user's top-level My Docs folder, which is what makes the walk possible with a user-level token — the admin-only GET /docs/users/{userId}/root is not needed.

Fetch a transcript

npm run transcript -- <noteId>
npm run transcript -- --latest          # newest note in the My Notes folder
npm run transcript -- <noteId> --json   # raw API response
npm run transcript -- <noteId> --vtt    # also emit WebVTT

Output goes to ./out/<slug>-<noteId>.md:

# In-person sync with the SI team

- note_id: `Nt4kPzQwRb6McD1sFgHy8A`
- note_url: https://docs.zoom.us/doc/9S1Vnf13RmqZ0ZYAD-k6uA
- segments: 412
- speakers: Michitaka Sugi, Speaker 1
- duration: ~37 min

## AI generated note
...

## Transcript

```
[00:00:21.743] Michitaka Sugi: では始めましょう。
```

React to my_notes.note_generated

Webhook:

npm run webhook          # listens on :4001/webhook
ngrok http 4001          # register https://<tunnel>/webhook in the app

The server answers the endpoint.url_validation challenge, verifies x-zm-signature against the raw body, acks in-line, and then pulls the transcript out of band.

WebSocket — same handler, no public URL:

# set ZOOM_WS_SUBSCRIPTION_ID first
npm run websocket

The event carries only note metadata (note_id, note_name, created_time, optional meeting_id). The transcript still comes from the content endpoint, using the token of the user who authorised the app — so an event about someone else's note will 403/404.

Notes and caveats

  • meeting_id is optional in the event. In-person recordings have no meeting, so its absence is the signal for "this came from a face-to-face session".
  • Transcript offsets, not timestamps. start_time / end_time are HH:MM:SS.mmm offsets from the start of the transcript.
  • speaker_id is meeting-scoped. The same person gets a different speaker_id in another meeting. zoom_user_id is present only when the speaker was matched to a Zoom user; otherwise you get Speaker 1-style names from voice recognition.
  • transcript may be absent even with include=transcript — the note simply has no transcript bound (e.g. a note typed by hand).
  • 413 Payload Too Large is a documented response on the content endpoint. Very long sessions can exceed the limit.
  • Folder naming. The My Notes folder label follows the client language. Override MY_NOTES_FOLDER_NAMES if yours is not My Notes.
  • Not supported in the Gov cluster (per the API reference).

Reference

License

MIT

About

Minimum PoC: fetch in-person recording transcripts from Zoom My Notes via the My Notes API and Canvas API (zero dependencies, Node 22)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages