Zoom REST API 架构指南:Base URL、区域路由、me关键字与 UUID 双重编码(knowledge-work-plugins zoom-plugin 实战解析)
【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins
本文以 knowledge-work-plugins 仓库中 zoom-plugin 的 rest-api 技能核心概念文档 api-architecture.md 为骨架,系统讲解调用 Zoom REST API 前必须掌握的设计约定:Base URL 与区域路由、me关键字的应用类型差异、Meeting ID 与 UUID 的选择及双重 URL 编码、ISO 8601 时间格式、录音下载 URL 的鉴权与重定向,以及共享访问权限等边界规则。读完本文,你将具备在 Server-to-Server OAuth 与 User OAuth 两种场景下正确拼装请求地址、规避常见 401/403/404 错误、安全下载云端录制文件的能力。
文档定位:为什么它是整个技能体系的"地基"
在 rest-api 技能中,api-architecture.md 被 SKILL.md 明确标注为FOUNDATION(基础)与"最关键的文档"之一,排在快速入门路径的第一步:先理解 API 设计(Base URL、区域端点、me关键字规则、ID 与 UUID、时间格式),再进入认证、会议生命周期、限流与 Webhook。也就是说,凡是基于 Zoom REST API 构建的服务端自动化(创建会议、管理用户、拉取录制、生成报表),都要先吃透本文的约定,否则后续每个请求都可能踩坑。
本文涉及的配套文档(以下均为仓库根目录相对路径):
- 认证流程:authentication-flows.md、认证参考
- 会议全生命周期示例:meeting-lifecycle.md
- 录制下载管线:recording-pipeline.md
- 限流策略:rate-limiting-strategy.md
- 错误排查:common-errors.md
- OAuth 完整实现:zoom-oauth 技能
Base URL:REST 与 GraphQL 两个独立入口
Zoom REST API 的所有请求都走 HTTPS,并在路径中携带 API 版本号/v2:
https://api.zoom.us/v2/例如创建会议、查询用户、拉取录制等 600+ 个 REST 端点都以此为基础前缀。在 SKILL.md 的快速开始中,创建会议的请求即形如:
curl -X POST "https://api.zoom.us/v2/users/HOST_USER_ID/meetings" \ -H "Authorization: Bearer ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{...}'而GraphQL使用独立的版本化端点,不在/v2之下:
https://api.zoom.us/v3/graphqlGraphQL 是单端点、基于游标分页的备选查询接口(beta),每个字段相当于一次 REST 调用对应的配额,具体用法可参见 rest-api 技能下的 graphql-queries.md 与 graphql 参考。
区域 Base URL:从 OAuth 令牌响应中读取api_url
Zoom 出于数据驻留(data residency)合规要求,允许不同地区的用户数据存放在对应的区域数据中心。OAuth 令牌响应中会返回一个api_url字段,指示该用户的数据所在区域:
{ "access_token": "eyJ...", "api_url": "https://api-eu.zoom.us" }正确的做法是:拿到api_url后,在其后追加/v2/构造出区域 Base URL。各区域对照如下:
| Region | API URL | Base URL |
|---|---|---|
| Global (default) | https://api.zoom.us | https://api.zoom.us/v2 |
| Australia | https://api-au.zoom.us | https://api-au.zoom.us/v2 |
| Canada | https://api-ca.zoom.us | https://api-ca.zoom.us/v2 |
| European Union | https://api-eu.zoom.us | https://api-eu.zoom.us/v2 |
| India | https://api-in.zoom.us | https://api-in.zoom.us/v2 |
| Saudi Arabia | https://api-sa.zoom.us | https://api-sa.zoom.us/v2 |
| Singapore | https://api-sg.zoom.us | https://api-sg.zoom.us/v2 |
| United Kingdom | https://api-uk.zoom.us | https://api-uk.zoom.us/v2 |
| United States | https://api-us.zoom.us | https://api-us.zoom.us/v2 |
| Vanity account | https://{vanity}.zoom.us | https://{vanity}.zoom.us/v2 |
重要提示:全局地址https://api.zoom.us无论用户位于哪个区域都始终可用。区域 URL 仅用于合规要求,并非强制——即使你忽略api_url直接使用全局地址,请求也能成功,只是可能不满足特定地区的数据驻留合规。
Node.js 实现:从令牌动态推导 Base URL
原始文档给出了一个完整的 Node.js 客户端工厂函数,它先换取令牌,再从api_url动态决定 Base URL,值得完整保留:
async function getZoomClient(accountId, clientId, clientSecret) { const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); const tokenRes = await fetch('https://zoom.us/oauth/token', { method: 'POST', headers: { 'Authorization': `Basic ${credentials}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: `grant_type=account_credentials&account_id=${accountId}` }); const tokenData = await tokenRes.json(); const baseUrl = tokenData.api_url ? `${tokenData.api_url}/v2` : 'https://api.zoom.us/v2'; return { accessToken: tokenData.access_token, baseUrl, async request(method, path, body = null) { const res = await fetch(`${this.baseUrl}${path}`, { method, headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined }); if (!res.ok) { const err = await res.json(); throw new Error(`Zoom API ${res.status}: ${err.message}`); } return res.json(); } }; }这段代码对应的是 Server-to-Server OAuth 的account_credentials授权方式(两脚 OAuth),令牌有效期 1 小时,expires_in为 3600 秒。如果要在生产环境做令牌缓存与自动刷新,可参考 authentication-flows.md 中带 60 秒缓冲的ZoomS2SAuth令牌管理器,以及 oauth 技能 中的 Redis 缓存、MySQL 存储与自动刷新示例。
me关键字:按应用类型严格区分,否则直接报错
URL 路径中的me是userId/accountId的替身,但它的行为因应用类型而异,这是 Zoom REST API 最容易踩的坑之一:
| App Type | meBehavior | When to Use |
|---|---|---|
| User-level OAuth | Resolves to the authenticated user | MUST use— providinguserIdcauses invalid token error |
| Server-to-Server OAuth | Not supported | MUST NOT use— provide actualuserIdor email |
| Account-level OAuth | Resolves to the user who installed the app | Can use eithermeoruserId |
示例
# User OAuth app — MUST use me GET /v2/users/me GET /v2/users/me/meetings # S2S OAuth app — MUST use actual userId or email GET /v2/users/abc123def GET /v2/users/john@example.com GET /v2/users/john@example.com/meetings常见错误与修复
如果你在 User OAuth 应用里误用了userId,会得到如下 401 错误:
{ "code": 4700, "message": "Invalid access token, does not contain scopes." }修复:把路径中的userId替换为me。同样的教训在 authentication-flows.md 中被再次强调("User OAuth appsmustusemeinstead ofuserId"),并在 common-errors.md 中作为 404(code 1001 "User does not exist")与 401(code 4700)的典型案例给出正误对照。在 SKILL.md 的快速开始中也能看到 S2S 场景的对应用法:POST /v2/users/HOST_USER_ID/meetings,即 S2S 必须显式写主机用户 ID 或邮箱,不能用me。
Meeting ID 与 UUID:语义不同,编码不同
- Meeting ID:会议的数字标识符,可复用于周期性会议(recurring meeting),最后一次使用后30 天过期。
- UUID:某个具体会议实例的唯一标识,永不过期,周期性会议的每次发生都会生成一个新的 UUID。
何时用哪个
| Use Case | Use |
|---|---|
| Get a scheduled meeting | Meeting ID |
| Get a past meeting instance | UUID |
| Get recordings for a specific session | UUID |
| Report on a specific occurrence | UUID |
在 meeting-lifecycle.md 的创建响应中可以看到两者同时出现:"id": 93123456789是 Meeting ID,"uuid": "xyzAbC1234=="是实例 UUID,join_url中携带的正是数字 ID。
UUID 双重 URL 编码(关键陷阱)
以/开头或包含//的 UUID必须双重 URL 编码。因为单次编码后路径中残留的%2F(表示/)会被部分服务再解码一次,导致路径语义被破坏。正确做法是调用两次encodeURIComponent。
function encodeUUID(uuid) { // Check if double-encoding is needed if (uuid.startsWith('/') || uuid.includes('//')) { return encodeURIComponent(encodeURIComponent(uuid)); } return encodeURIComponent(uuid); } // UUID: /abcABC123== // Single encode: %2FabcABC123%3D%3D // Double encode: %252FabcABC123%253D%253D ← Required const meetingUUID = '/abcABC123=='; const url = `https://api.zoom.us/v2/past_meetings/${encodeUUID(meetingUUID)}`;Python 侧使用urllib.parse.quote实现同样的双重编码(注意safe='',确保/与=都被编码):
from urllib.parse import quote def encode_uuid(uuid_str): if uuid_str.startswith('/') or '//' in uuid_str: return quote(quote(uuid_str, safe=''), safe='') return quote(uuid_str, safe='') uuid = '/abcABC123==' url = f'https://api.zoom.us/v2/past_meetings/{encode_uuid(uuid)}'在 common-errors.md 中,"Meeting not found"(code 300)的排查清单里也明确包含"UUID 编码错误",其解法正是双重编码。因此当你遇到 404 且 ID 看起来像 UUID 时,第一反应应是检查编码方式。
时间格式:UTC 与本地时间的 ISO 8601 两种变体
Zoom API 使用 ISO 8601,但存在两种变体,混用是字段校验失败的常见原因:
| Format | Meaning | Example |
|---|---|---|
yyyy-MM-ddTHH:mm:ssZ | UTC time(Z suffix) | 2025-03-15T10:00:00Z |
yyyy-MM-ddTHH:mm:ss | Local time(no Z, usestimezonefield) | 2025-03-15T10:00:00 |
设置会议时间
方式一:本地时间 +timezone字段(推荐,避免时区换算错误):
{ "topic": "Team Meeting", "type": 2, "start_time": "2025-03-15T10:00:00", "timezone": "America/Los_Angeles", "duration": 60 }方式二:直接用 UTC 时间(带Z后缀,无需timezone):
{ "topic": "Team Meeting", "type": 2, "start_time": "2025-03-15T17:00:00Z", "duration": 60 }注意:部分 Report API 只接受 UTC 格式。每次调用前务必查阅对应端点的参考文档确认接受的格式。在 meeting-lifecycle.md 的创建示例中,同时出现了"start_time": "2025-03-15T10:00:00Z"与"timezone": "America/New_York"的混用形态——这种写法在实操中可用,但保持"本地时间 + timezone"或"纯 UTC"二选一的风格,能最大程度减少歧义。
纯日期参数
部分端点(如录制列表)使用YYYY-MM-DD纯日期格式:
GET /v2/users/me/recordings?from=2025-01-01&to=2025-01-31这与 recordings.md 中记录的一致:from/to为YYYY-MM-DD格式的起止日期。注意录制处理有延迟,会议结束到录制可查询之间存在处理时间,不要假设"刚结束就能拉到"——这是 recording-pipeline.md 明确列出的常见陷阱之一。
Download URL:动态生成、必须鉴权、必须跟随重定向
API 响应与 Webhook 负载中的录制download_url是动态生成的,需要认证才能访问,且可能发生 301/302 重定向。
认证方式
- 在 Authorization 头中携带 Bearer token(推荐):
curl -L -H "Authorization: Bearer ACCESS_TOKEN" \ "https://zoom.us/rec/archive/download/xyz"- 使用 Webhook 负载中的
download_access_token(用于 Webhook 触发的下载场景):
curl -L -H "Authorization: Bearer DOWNLOAD_ACCESS_TOKEN" \ "https://zoom.us/rec/archive/download/xyz"跟随重定向
Download URL 可能返回 301/302 重定向,必须始终跟随:
// Node.js — fetch follows redirects by default const response = await fetch(downloadUrl, { headers: { 'Authorization': `Bearer ${accessToken}` }, redirect: 'follow' }); const fileBuffer = await response.arrayBuffer();# Python — requests follows redirects by default import requests response = requests.get( download_url, headers={'Authorization': f'Bearer {access_token}'}, allow_redirects=True, stream=True ) with open('recording.mp4', 'wb') as f: for chunk in response.iter_content(chunk_size=8192): f.write(chunk)在 SKILL.md 的关键陷阱一节同样强调:download_url需要 Bearer token 认证,且要跟随重定向(curl -L)。recording-pipeline.md 进一步把"不附加 Bearer token 就跟随download_url"和"不处理重定向响应"列为最常见的两大坑。完整的"Webhook 触发 → 拉取录制列表 → 鉴权下载 → 存入对象存储"流水线可参考该文件。
Personal Meeting ID(PMI)的特殊之处
用户可以用自己的 PMI(个人会议室 ID)创建会议。API 响应中返回的是唯一的会议 ID,但Webhook 事件仍然引用 PMI。因此,对于基于 PMI 的会议,向 API 端点传 ID 时应使用 PMI 本身。这类会议对应 meeting-lifecycle.md 中的type: 3(Recurring with no fixed time,即 PMI 会议)。
共享访问权限:代他人操作的前置条件
拥有 Schedule Privilege(安排权限)或基于角色的访问权限的用户,可以代表其他用户执行操作。如果应用要访问"非安装应用的用户"的资源,该用户必须已授权共享访问权限。
未授权时的报错:
{ "code": 403, "message": "authenticated user has not permitted access to the targeted resource" }解决方式:引导用户在其 Zoom 设置中开启共享访问权限。同样的错误模式(code 3001 / 403)也在 common-errors.md 的"Shared Access Permissions"场景中被记录:先确认用户角色(Admin/Owner),再检查端点是否需要 admin 级 scope(如meeting:write:admin而非meeting:write)。
邮箱显示规则:外部参会者邮箱何时可见
外部参会者的邮箱仅在满足以下任一条件时才被展示:
- 参会者在注册时填写了邮箱
- 主持人通过日历集成、身份验证例外或分组讨论室(breakout room)分配提供了邮箱
- 为网络研讨会主持人/参会者导入了 CSV
这意味着:在拉取参会者列表或报表时,不要假设所有外部参会者的邮箱字段都有值——它受上述规则约束,缺失是正常现象。
高失败率风险与请求鉴权
如果应用持续保持很高的错误/请求比率,Zoom 可能直接禁用该应用。因此必须构建健壮的错误处理与优雅的重试逻辑。可参考 rate-limiting-strategy.md 中给出的三种策略:指数退避 + 抖动(处理 429)、基于X-RateLimit-Remaining的主动限速、以及高并发场景的请求队列;并注意"限流按账号而非按应用统计""会议/网络研讨会创建与更新每用户每天 100 次(00:00 UTC 重置)"等硬约束。
所有 API 请求都要求在Authorization头中携带 Bearer token:
Authorization: Bearer {access_token}完整的认证实现参见 authentication-flows.md(涵盖 S2S、User OAuth/PKCE、Device Code 四种流程的选型、令牌刷新与错误处理),或直接使用zoom-oauth技能中的完整代码示例。
实战速查:把约定固化成编码习惯
| 关注点 | 正确姿势 | 错误姿势 |
|---|---|---|
| Base URL | 从 OAuth 响应api_url推导,追加/v2 | 一律硬编码https://api.zoom.us/v2(合规场景不满足) |
me关键字 | User OAuth 用me;S2S 用真实 userId/email | User OAuth 传 userId(报 4700);S2S 传me |
| UUID 编码 | 以/开头或含//时双重编码 | 仅单次encodeURIComponent(报 404) |
| 时间格式 | 本地时间 +timezone,或纯 UTC(Z) | 无Z又不带timezone(字段校验失败) |
| 录制下载 | Bearer 鉴权 + 跟随重定向 | 匿名访问或忽略 301/302 |
| 共享访问 | 确保目标用户已授权共享权限 | 默认可访问他人资源(报 403) |
| 失败率 | 指数退避、优雅重试、监控限流头 | 无限重试、持续报错(应用被禁用) |
下一步阅读
- 想跑通第一个请求:meeting-lifecycle.md 提供了 Create → Update → Get → List → Delete 的完整 curl 与 Node.js 示例
- 想拿令牌:authentication-flows.md 或 zoom-oauth
- 想处理限流与 429:rate-limiting-strategy.md
- 想自动下载录制:recording-pipeline.md
- 遇到错误:common-errors.md 与 rest-api 的 RUNBOOK.md
- 全部端点域参考:rest-api 技能下的 references 目录(39 个按 API 域划分的端点清单,与官方 API Hub 的
endpoints.json清单对齐)
【免费下载链接】knowledge-work-pluginsOpen source repository of plugins primarily intended for knowledge workers to use in Claude Cowork项目地址: https://gitcode.com/GitHub_Trending/kn/knowledge-work-plugins
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考