
Zoom General
- 1.5k installs
- 23.3k repo stars
- Updated August 5, 2026
- anthropics/knowledge-work-plugins
zoom-general is an agent skill for cross-product zoom reference skill. use after the workflow is clear when you need shared platform guidance, app-model comparisons, authentication context, scopes, marketplace.
About
The zoom-general skill is designed for cross-product Zoom reference skill. Use after the workflow is clear when you need shared platform guidance, app-model comparisons, authentication context, scopes, marketplace. Zoom General (Cross-Product Skills) Background reference for cross-product Zoom questions. Prefer the workflow skills first, then use this file for shared platform guidance and routing detail. Invoke when the user asks about zoom general or related SKILL.md workflows.
- Create a meeting, configure webhooks, and handle OAuth token refresh ->.
- Build a custom video UI for a Zoom meeting on web ->.
- Rivet SDK is a Node.js framework that bundles Zoom auth handling, webhook receivers, and typed API wrappers.
- If user chooses Rivet: chain rivet-sdk + oauth + rest-api.
- If user declines Rivet: chain oauth + rest-api (+ webhooks or product skill as needed).
Zoom General by the numbers
- 1,491 all-time installs (skills.sh)
- +73 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #268 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
zoom-general capabilities & compatibility
- Capabilities
- create a meeting, configure webhooks, and handle · build a custom video ui for a zoom meeting on we · rivet sdk is a node.js framework that bundles zo · if user chooses rivet: chain rivet sdk + oauth +
- Use cases
- frontend
What zoom-general says it does
Cross-product Zoom reference skill. Use after the workflow is clear when you need shared platform guidance, app-model comparisons, authentication context, scopes, marketplace consi
Cross-product Zoom reference skill. Use after the workflow is clear when you need shared platform guidance, app-model comparisons, authentication context, scope
npx skills add https://github.com/anthropics/knowledge-work-plugins --skill zoom-generalAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.5k |
|---|---|
| repo stars | ★ 23.3k |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | anthropics/knowledge-work-plugins ↗ |
How do I cross-product zoom reference skill. use after the workflow is clear when you need shared platform guidance, app-model comparisons, authentication context, scopes, marketplace?
Cross-product Zoom reference skill. Use after the workflow is clear when you need shared platform guidance, app-model comparisons, authentication context, scopes, marketplace.
Who is it for?
Developers using zoom general workflows documented in SKILL.md.
Skip if: Skip when the task falls outside zoom-general scope or needs a different stack.
When should I use this skill?
User asks about zoom general or related SKILL.md workflows.
What you get
Completed zoom-general workflow with documented commands, files, and expected deliverables.
- app type selection
- OAuth scope plan
By the numbers
- Documents 3 Zoom Marketplace app types
Files
Zoom General (Cross-Product Skills)
Background reference for cross-product Zoom questions. Prefer the workflow skills first, then use this file for shared platform guidance and routing detail.
How zoom-general Routes a Complex Developer Query
Use zoom-general as the classifier and chaining layer:
1. detect product signals in the query 2. pick one primary skill 3. attach secondary skills for auth, events, or deployment edges 4. ask one short clarifier only when two routes match with similar confidence
Minimal implementation:
type SkillId =
| 'zoom-general'
| 'zoom-rest-api'
| 'zoom-webhooks'
| 'zoom-oauth'
| 'zoom-meeting-sdk-web-component-view'
| 'zoom-video-sdk'
| 'zoom-mcp';
const hasAny = (q: string, words: string[]) => words.some((w) => q.includes(w));
function detectSignals(rawQuery: string) {
const q = rawQuery.toLowerCase();
return {
meetingCustomUi: hasAny(q, ['zoom meeting', 'custom ui', 'component view', 'embed meeting']),
customVideo: hasAny(q, ['video sdk', 'custom video session', 'peer-video-state-change']),
restApi: hasAny(q, ['rest api', '/v2/', 'create meeting', 'list users', 's2s oauth']),
webhooks: hasAny(q, ['webhook', 'x-zm-signature', 'event subscription', 'crc']),
oauth: hasAny(q, ['oauth', 'pkce', 'token refresh', 'account_credentials']),
mcp: hasAny(q, ['zoom mcp', 'agentic retrieval', 'tools/list', 'semantic meeting search']),
};
}
function pickPrimarySkill(s: ReturnType<typeof detectSignals>): SkillId {
if (s.meetingCustomUi) return 'zoom-meeting-sdk-web-component-view';
if (s.mcp) return 'zoom-mcp';
if (s.restApi) return 'zoom-rest-api';
if (s.customVideo) return 'zoom-video-sdk';
return 'zoom-general';
}
function buildChain(primary: SkillId, s: ReturnType<typeof detectSignals>): SkillId[] {
const chain = [primary];
if (s.oauth && !chain.includes('zoom-oauth')) chain.push('zoom-oauth');
if (s.webhooks && !chain.includes('zoom-webhooks')) chain.push('zoom-webhooks');
return chain;
}Example:
Create a meeting, configure webhooks, and handle OAuth token refresh->
zoom-rest-api -> zoom-oauth -> zoom-webhooks
Build a custom video UI for a Zoom meeting on web->
zoom-meeting-sdk-web-component-view
For the full TypeScript implementation and handoff contract, use references/routing-implementation.md.
Choose Your Path
| I want to... | Use this skill |
|---|---|
| Build a custom web UI around a real Zoom meeting | [zoom-meeting-sdk-web-component-view](../meeting-sdk/web/component-view/SKILL.md) |
| Build deterministic automation/configuration/reporting with explicit request control | [zoom-rest-api](../rest-api/SKILL.md) |
| Receive event notifications (HTTP push) | [zoom-webhooks](../webhooks/SKILL.md) |
| Receive event notifications (WebSocket, low-latency) | [zoom-websockets](../websockets/SKILL.md) |
| Embed Zoom meetings in my app | [zoom-meeting-sdk](../meeting-sdk/SKILL.md) |
| Build custom video experiences (Web, React Native, Flutter, Android, iOS, macOS, Unity, Linux) | [zoom-video-sdk](../video-sdk/SKILL.md) |
| Build an app that runs inside Zoom client | [zoom-apps-sdk](../zoom-apps-sdk/SKILL.md) |
| Transcribe uploaded or stored media with AI Services Scribe | [scribe](../scribe/SKILL.md) |
| Access live audio/video/transcripts from meetings | [zoom-rtms](../rtms/SKILL.md) |
| Enable collaborative browsing for support | [zoom-cobrowse-sdk](../cobrowse-sdk/SKILL.md) |
| Build Contact Center apps and channel integrations | [contact-center](../contact-center/SKILL.md) |
| Build Virtual Agent web/mobile chatbot experiences | [virtual-agent](../virtual-agent/SKILL.md) |
| Build Zoom Phone integrations (Smart Embed, Phone API, webhooks, URI flows) | [phone](../phone/SKILL.md) |
| Build Team Chat apps and integrations | [zoom-team-chat](../team-chat/SKILL.md) |
| Build server-side integrations with Rivet (auth + webhooks + APIs) | [rivet-sdk](../rivet-sdk/SKILL.md) |
| Run browser/device/network preflight diagnostics before join | [probe-sdk](../probe-sdk/SKILL.md) |
| Add pre-built UI components for Video SDK | [zoom-ui-toolkit](../ui-toolkit/SKILL.md) |
| Implement OAuth authentication (all grant types) | [zoom-oauth](../oauth/SKILL.md) |
| Build AI-driven tool workflows (AI Companion/agents) over Zoom data | [zoom-mcp](../zoom-mcp/SKILL.md) |
| Build AI-driven Whiteboard workflows over Zoom Whiteboard MCP | [zoom-mcp/whiteboard](../zoom-mcp/whiteboard/SKILL.md) |
| Build enterprise AI systems with stable API core + AI tool layer | [zoom-rest-api](../rest-api/SKILL.md) + [zoom-mcp](../zoom-mcp/SKILL.md) |
Planning Checkpoint: Rivet SDK (Optional)
When a user starts planning a server-side integration that combines auth + webhooks + API calls, ask this first:
Rivet SDK is a Node.js framework that bundles Zoom auth handling, webhook receivers, and typed API wrappers.Do you want to use Rivet SDK for faster scaffolding, or do you prefer a direct OAuth + REST implementation without Rivet?
Routing after answer:
- If user chooses Rivet: chain
rivet-sdk+oauth+rest-api. - If user declines Rivet: chain
oauth+rest-api(+webhooksor product skill as needed).
SDK vs REST Routing Matrix (Hard Stop)
| User intent | Correct path | Do not route to |
|---|---|---|
| Embed Zoom meeting in app UI | zoom-meeting-sdk | REST-only join_url flow |
| Build custom web UI for a real Zoom meeting | zoom-meeting-sdk-web-component-view | zoom-video-sdk |
| Build custom video UI/session app | zoom-video-sdk | Meeting SDK or REST meeting links |
| Get browser join links / manage meeting resources | zoom-rest-api | Meeting SDK join implementation |
Routing guardrails:
- If user asks for SDK embed/join behavior, stay in SDK path.
- If the prompt says meeting plus custom UI/video/layout/embed, prefer
zoom-meeting-sdk-web-component-view. - Only use
zoom-video-sdkwhen the user is building a custom session product rather than a Zoom meeting. - Only use REST path for resource management, reporting, or link distribution unless user explicitly requests a mixed architecture.
- For executable classification/chaining logic and error handling, see references/routing-implementation.md.
API vs MCP Routing Matrix (Hard Stop)
| User intent | Correct path | Why |
|---|---|---|
| Deterministic backend automation, account/user configuration, reporting, scheduled jobs | zoom-rest-api | Explicit request/response control and repeatable behavior |
| AI agent chooses tools dynamically, cross-platform AI tool interoperability | zoom-mcp | MCP is optimized for dynamic tool discovery and agentic workflows |
| Enterprise AI architecture (stable core + adaptive AI layer) | zoom-rest-api + zoom-mcp | APIs run core system actions; MCP exposes curated AI tools/context |
Routing guardrails:
- Do not replace deterministic backend APIs with MCP-only routing.
- Do not force raw REST-first routing when the task is AI-agent tool orchestration.
- Prefer hybrid routing when the user needs both stable automation and AI-driven interactions.
- MCP remote server works over Streamable HTTP/SSE; use this path when the target client/agent supports MCP transports (for example Claude or VS Code).
- Do not design per-tenant custom MCP endpoint provisioning; Zoom MCP endpoints are shared at instance/cluster level.
- Source: https://developers.zoom.us/docs/mcp/library/resources/apis-vs-mcp/
Ambiguity Resolution (Ask Before Routing)
When a prompt matches both API and MCP paths with similar confidence, ask one short clarifier before execution:
Do you want deterministic REST API automation, AI-agent MCP tooling, or a hybrid of both?
Then route as:
- REST answer →
zoom-rest-api - MCP answer →
zoom-mcp - Hybrid answer →
zoom-rest-api + zoom-mcp
MCP Availability and Topology Notes
- Zoom-hosted MCP access is evolving; docs indicate a model where Zoom exposes product-scoped MCP servers (for example Meetings, Team Chat, Whiteboard).
- Use
zoom-mcpas the parent MCP entry point. - Route Whiteboard-specific MCP requests to [zoom-mcp/whiteboard](../zoom-mcp/whiteboard/SKILL.md).
- When a request is product-specific and MCP coverage exists, route to that MCP product surface first; otherwise use REST/SDK skills for deterministic implementation.
Webhooks vs WebSockets
Both receive event notifications, but differ in approach:
| Aspect | webhooks | zoom-websockets |
|---|---|---|
| Connection | HTTP POST to your endpoint | Persistent WebSocket |
| Latency | Higher | Lower |
| Security | Requires public endpoint | No exposed endpoint |
| Setup | Simpler | More complex |
| Best for | Most use cases | Real-time, security-sensitive |
Common Use Cases
| Use Case | Description | Skills Needed |
|---|---|---|
| Meeting + Webhooks + OAuth Refresh | Create a meeting, process real-time updates, and refresh OAuth tokens safely in one design | zoom-rest-api + zoom-oauth + zoom-webhooks |
| Scribe Transcription Pipeline | Transcribe uploaded files or S3 archives with AI Services Scribe using fast mode or batch jobs | scribe + optional zoom-rest-api + optional zoom-webhooks |
| APIs vs MCP Routing | Decide whether to route to deterministic Zoom APIs, AI-driven MCP, or a hybrid design | zoom-rest-api and/or zoom-mcp |
| Custom Meeting UI (Web) | Build a custom video UI for a real Zoom meeting in a web app using Meeting SDK Component View | zoom-meeting-sdk-web-component-view + zoom-oauth |
| Meeting Automation | Schedule, update, delete meetings programmatically | zoom-rest-api |
| Meeting Bots | Build bots that join meetings for AI/transcription/recording | meeting-sdk/linux + zoom-rest-api + optional zoom-webhooks |
| High-Volume Meeting Platform | Design distributed meeting creation and event processing with retries, queues, and reconciliation | zoom-rest-api + zoom-webhooks + zoom-oauth |
| Recording & Transcription | Download recordings, get transcripts | zoom-webhooks + zoom-rest-api |
| Recording Download Pipeline | Auto-download recordings to your own storage (S3, GCS, etc.) | zoom-webhooks + zoom-rest-api |
| Real-Time Media Streams | Access live audio, video, transcripts via WebSocket | zoom-rtms + zoom-webhooks |
| In-Meeting Apps | Build apps that run inside Zoom meetings | zoom-apps-sdk + zoom-oauth |
| React Native Meeting Embed | Embed meetings into iOS/Android React Native apps | zoom-meeting-sdk-react-native + zoom-oauth |
| Native Meeting SDK Multi-Platform Delivery | Align Android, iOS, macOS, and Unreal Meeting SDK implementations under one auth/version strategy | zoom-meeting-sdk + platform skills |
| Native Video SDK Multi-Platform Delivery | Align Android, iOS, macOS, and Unity Video SDK implementations under one auth/version strategy | zoom-video-sdk + platform skills |
| Electron Meeting Embed | Embed meetings into desktop Electron apps | zoom-meeting-sdk-electron + zoom-oauth |
| Flutter Video Sessions | Build custom mobile video sessions in Flutter | zoom-video-sdk-flutter + zoom-oauth |
| React Native Video Sessions | Build custom mobile video sessions in React Native | zoom-video-sdk-react-native + zoom-oauth |
| Immersive Experiences | Custom video layouts with Layers API | zoom-apps-sdk |
| Collaborative Apps | Real-time shared state in meetings | zoom-apps-sdk |
| Contact Center App Lifecycle and Context Switching | Build Contact Center apps that handle engagement events and multi-engagement state | contact-center + zoom-apps-sdk |
| Virtual Agent Campaign Web and Mobile Wrapper | Deliver one campaign-driven bot flow across web and native mobile wrappers | virtual-agent + contact-center |
| Virtual Agent Knowledge Base Sync Pipeline | Sync external knowledge content into Zoom Virtual Agent using web sync or custom API connectors | virtual-agent + zoom-rest-api + zoom-oauth |
| Zoom Phone Smart Embed CRM Integration | Build CRM dialer and call logging flows using Smart Embed plus Phone APIs | phone + zoom-oauth + zoom-webhooks |
| Rivet Event-Driven API Orchestrator | Build a Node.js backend that combines webhooks and API actions through Rivet module clients | rivet-sdk + zoom-oauth + zoom-rest-api |
| Probe SDK Preflight Readiness Gate | Add browser/device/network diagnostics and readiness policy before Meeting SDK or Video SDK joins | probe-sdk + zoom-meeting-sdk or zoom-video-sdk |
Complete Use-Case Index
- APIs vs MCP Routing: choose API-only, MCP-only, or hybrid routing using official Zoom criteria.
- AI Companion Integration: connect Zoom AI Companion capabilities into your app workflow.
- AI Integration: add summarization, transcription, or assistant logic using Zoom data surfaces.
- Backend Automation (S2S OAuth): run server-side jobs with account-level OAuth credentials.
- Collaborative Apps: build shared in-meeting app state and interactions.
- Contact Center Integration: connect Zoom Contact Center signals into external systems.
- Contact Center App Lifecycle and Context Switching: implement event-driven engagement state and safe context switching in Contact Center apps.
- Virtual Agent Campaign Web and Mobile Wrapper: deploy campaign-based Virtual Agent chat across website and Android/iOS WebView wrappers.
- Virtual Agent Knowledge Base Sync Pipeline: automate knowledge-base ingestion with web sync strategy or custom API connector.
- Zoom Phone Smart Embed CRM Integration: integrate Smart Embed events, Phone APIs, and CRM workflows with migration-safe data handling.
- Rivet Event-Driven API Orchestrator: build a Node.js backend that combines webhook handling and API orchestration with Rivet.
- Probe SDK Preflight Readiness Gate: run browser/device/network diagnostics before launching meeting or video session workflows.
- Custom Video: decide between Video SDK and related components for custom session UX.
- Custom Meeting UI (Web): use Meeting SDK Component View for a custom UI around a real Zoom meeting.
- Scribe Transcription Pipeline: use AI Services Scribe for on-demand file transcription and batch archive processing.
- Video SDK Bring Your Own Storage: configure Video SDK cloud recordings to write directly to your own S3 bucket.
- Customer Support Cobrowsing: implement customer-agent collaborative browsing support flows.
- Embed Meetings: embed Zoom meeting experience into your app.
- Form Completion Assistant: build guided flows for form filling and completion assistance.
- HD Video Resolution: enable and troubleshoot high-definition video requirements.
- High-Volume Meeting Platform: build distributed meeting creation and event processing with concrete fallback patterns.
- Immersive Experiences: use Zoom Apps Layers APIs for custom in-meeting visuals.
- In-Meeting Apps: build Zoom Apps that run directly inside meeting and webinar contexts.
- Marketplace Publishing: prepare and ship a Zoom app through Marketplace review.
- Meeting Automation: create, update, and manage meetings programmatically.
- Meeting Bots: build bots for meeting join, capture, and real-time analysis.
- Native Meeting SDK Multi-Platform Delivery: standardize Android, iOS, macOS, and Unreal Meeting SDK delivery with shared auth and version controls.
- Native Video SDK Multi-Platform Delivery: standardize Android, iOS, macOS, and Unity Video SDK delivery with shared auth and version controls.
- Meeting Details with Events: combine REST retrieval with webhook event streams.
- Minutes Calculation: compute usage and minute metrics across meetings/sessions.
- Prebuilt Video UI: use UI Toolkit for faster Video SDK-based UI delivery.
- QSS Monitoring: monitor Zoom quality statistics and performance indicators.
- Raw Recording: capture raw streams for custom recording and processing pipelines.
- Electron Meeting Embed: embed meetings in an Electron desktop application.
- Flutter Video Sessions: build Video SDK sessions in Flutter mobile apps.
- React Native Meeting Embed: embed Meeting SDK into React Native apps.
- React Native Video Sessions: build custom video sessions in React Native.
- Real-Time Media Streams: consume live media/transcript streams via RTMS.
- Recording Download Pipeline: automate recording retrieval and storage pipelines.
- Recording & Transcription: manage post-meeting recordings and transcript workflows.
- Retrieve Meeting and Subscribe Events: join REST meeting fetch with event subscriptions.
- SaaS App OAuth Integration: implement user-level OAuth in multi-tenant SaaS apps.
- SDK Size Optimization: reduce bundle/runtime footprint for SDK-based apps.
- SDK Wrappers and GUI: evaluate wrapper patterns and GUI frameworks around SDKs.
- Team Chat LLM Bot: build a Team Chat bot with LLM-powered responses.
- Testing and Development: local testing patterns, mocks, and safe development loops.
- Token and Scope Troubleshooting: debug OAuth scope and token mismatch issues.
- Transcription Bot (Linux): run Linux meeting bots for live transcription workloads.
- Usage Reporting and Analytics: collect and analyze usage/reporting data.
- User and Meeting Creation: provision users and schedule meetings in one flow.
- Web SDK Embedding: embed meeting experiences in browser-based web apps.
- Server-to-Server OAuth with Webhooks: combine account OAuth with event-driven backend processing.
- Meeting Links vs Embedding: choose between
join_urldistribution and SDK embedding. - Enterprise App Deployment: deploy, govern, and operate Zoom integrations at enterprise scale.
Prerequisites
1. Zoom account (Pro, Business, or Enterprise) 2. App created in Zoom App Marketplace 3. OAuth credentials (Client ID and Secret)
References
- Known Limitations & Quirks
Quick Start
1. Go to marketplace.zoom.us 2. Click Develop → Build App 3. Select app type (see references/app-types.md) 4. Configure OAuth and scopes 5. Copy credentials to your application
Detailed References
- [references/authentication.md](references/authentication.md) - OAuth 2.0, S2S OAuth, JWT patterns
- [references/app-types.md](references/app-types.md) - Decision guide for app types
- [references/scopes.md](references/scopes.md) - OAuth scopes reference
- [references/marketplace.md](references/marketplace.md) - Marketplace portal navigation
- [references/query-routing-playbook.md](references/query-routing-playbook.md) - Route complex queries to the right specialized skills
- [references/interview-answer-routing.md](references/interview-answer-routing.md) - Short interview-ready answer pattern for zoom-general routing
- [references/routing-implementation.md](references/routing-implementation.md) - Concrete TypeScript query classification and skill handoff contract
- [references/automatic-skill-chaining-rest-webhooks.md](references/automatic-skill-chaining-rest-webhooks.md) - Executable process for REST + webhook chained workflows
- [references/meeting-webhooks-oauth-refresh-orchestration.md](references/meeting-webhooks-oauth-refresh-orchestration.md) - Concrete design for meeting creation + webhook updates + OAuth token refresh
- [references/distributed-meeting-fallback-architecture.md](references/distributed-meeting-fallback-architecture.md) - High-volume distributed architecture with retries, circuit breakers, and reconciliation fallbacks
- [references/community-repos.md](references/community-repos.md) - Curated official Zoom sample repositories by product
SDK Maintenance
- [references/sdk-upgrade-guide.md](references/sdk-upgrade-guide.md) - Version policy, upgrade steps
- [references/sdk-upgrade-workflow.md](references/sdk-upgrade-workflow.md) - Changelog + RSS, version-by-version reusable upgrade workflow
- [references/sdk-logs-troubleshooting.md](references/sdk-logs-troubleshooting.md) - Collecting SDK logs
Resources
- Official docs: https://developers.zoom.us/
- Marketplace: https://marketplace.zoom.us/
- Developer forum: https://devforum.zoom.us/
Environment Variables
- See references/environment-variables.md for standardized
.envkeys and where to find each value.
Operations
- RUNBOOK.md - 5-minute preflight and debugging checklist.
App Types
Choose the right Zoom app type for your integration.
Overview
Zoom Marketplace has 3 app types:
| App Type | Use Case |
|---|---|
| General App | Flexible - configure surfaces, embeds, OAuth, webhooks |
| Server-to-Server OAuth | Backend automation, no user authorization |
| Webhook Only | Receive events only, no API access |
General App
The modular app type. Pick what you need:
OAuth Type (choose one)
| Type | Scopes | Authorization |
|---|---|---|
| Admin | Admin scopes (*:admin) | Entire account OR specific users |
| User | User scopes | Only themselves (self-service) |
Surfaces (product contexts)
Your app can interact with these Zoom products:
- Meetings
- Webinars
- Rooms
- Phone
- Team Chat
- Contact Center
- Whiteboard
- Virtual Agent
- Events
- Workflows
Embeds (SDKs)
Embed Zoom functionality in your app:
| Embed | Description |
|---|---|
| Meeting SDK | Embed Zoom meetings |
| Contact Center SDK | Embed Contact Center |
| Phone SDK | Embed Phone functionality |
Access
Configure in the Access tab:
- Secret Token - Verify webhook notifications
- Event Subscription - Webhooks
- WebSockets - Real-time event connections
Scopes
Define which API methods the app can call. Scopes are:
- Restricted to specific resources
- Reviewed by Zoom during app submission
Features in General App
General App can also include:
- Zoom Apps - Apps that run inside Zoom client
Server-to-Server OAuth
Backend automation without user authorization.
- No user interaction required
- Access your account's data
- Can include webhooks and zoom-websockets
- Best for: automation, reporting, integrations
Webhook Only
Event notifications only.
- Receive events, no API calls
- No OAuth tokens needed
- Best for: event logging, triggering external workflows
Use this when you ONLY need events. Otherwise, add webhooks to General App or S2S.
Decision Guide
| Need | App Type |
|---|---|
| Call APIs for your account (backend) | Server-to-Server OAuth |
| Call APIs on behalf of users | General App (Admin or User OAuth) |
| Embed Zoom meetings | General App + Meeting SDK embed |
| Embed Contact Center | General App + Contact Center SDK embed |
| Embed Phone | General App + Phone SDK embed |
| Build in-client app | General App + Zoom Apps |
| Receive events only | Webhook Only |
| Receive events + call APIs | General App or S2S (with webhooks) |
Resources
- App types docs: https://developers.zoom.us/docs/integrations/
- Marketplace: https://marketplace.zoom.us/
Authentication
Authentication methods for Zoom APIs and SDKs.
Overview
Zoom supports multiple authentication methods depending on your use case:
| Method | Use Case |
|---|---|
| OAuth 2.0 | User-authorized access (on behalf of user) |
| Server-to-Server OAuth | Server-side automation (no user interaction) |
| SDK JWT | Meeting SDK and Video SDK authentication |
OAuth 2.0
For apps that act on behalf of users.
Flow
1. User clicks "Connect with Zoom"
2. Redirect to Zoom authorization URL
3. User grants permission
4. Zoom redirects back with auth code
5. Exchange code for access token
6. Use token to call APIsAuthorization URL
https://zoom.us/oauth/authorize?response_type=code&client_id={clientId}&redirect_uri={redirectUri}Token Exchange
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic {base64(clientId:clientSecret)}" \
-d "grant_type=authorization_code&code={authCode}&redirect_uri={redirectUri}"Server-to-Server OAuth
For server-side automation without user interaction.
Get Access Token
curl -X POST "https://zoom.us/oauth/token?grant_type=account_credentials&account_id={accountId}" \
-H "Authorization: Basic {base64(clientId:clientSecret)}"Response
{
"access_token": "eyJ...",
"token_type": "bearer",
"expires_in": 3600
}SDK JWT Signatures
For Meeting SDK and Video SDK authentication. See:
- Meeting SDK Authorization
- Video SDK Authorization
Best Practices
| Practice | Recommendation |
|---|---|
| Expiry (`exp`) | Set ~10 seconds after generation |
| Issued At (`iat`) | Set 2 hours in the past (if exp - iat >= 2 hours required) |
| Generate server-side | Never expose secrets in client code |
Resources
- OAuth docs: https://developers.zoom.us/docs/integrations/oauth/
- S2S OAuth docs: https://developers.zoom.us/docs/internal-apps/s2s-oauth/
Authorization Patterns
Permission validation middleware and role-based access control for Zoom API integrations.
Note: These are implementation patterns for YOUR application when building Zoom integrations. These are not Zoom's internal authorization mechanisms - they are examples of how to structure authorization logic in your own backend.
Overview
When chaining multiple Zoom API calls, each step may require different scopes and permissions. This document provides patterns for validating authorization at each step before proceeding.
Authorization Flow
┌─────────────────────────────────────────────────────────────────────────┐
│ AUTHORIZATION VALIDATION FLOW │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. Check Token Validity │
│ └── Is token expired? → Refresh or re-authenticate │
│ └── Is token revoked? → Re-authenticate │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 2. Validate Required Scopes │
│ └── Does token have scopes for this operation? │
│ └── If missing → Return 403 with required scopes │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 3. Check Resource Permissions │
│ └── Does user have access to this resource? │
│ └── Is user admin/owner/member? │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 4. Execute Operation │
│ └── Call Zoom API │
│ └── Handle API-level authorization errors │
└─────────────────────────────────────────────────────────────────────────┘Scope Validation Middleware
Express.js Middleware
const axios = require('axios');
/**
* Middleware to validate OAuth token has required scopes
* @param {string[]} requiredScopes - Scopes required for this route
*/
function requireScopes(requiredScopes) {
return async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
error: 'unauthorized',
message: 'No access token provided'
});
}
try {
// Get token info to check scopes
const tokenInfo = await getTokenInfo(token);
// Check if token has all required scopes
const tokenScopes = tokenInfo.scope.split(' ');
const missingScopes = requiredScopes.filter(
scope => !tokenScopes.includes(scope)
);
if (missingScopes.length > 0) {
return res.status(403).json({
error: 'insufficient_scope',
message: 'Token missing required scopes',
required_scopes: requiredScopes,
missing_scopes: missingScopes,
your_scopes: tokenScopes
});
}
// Attach token info to request for downstream use
req.zoomToken = tokenInfo;
req.zoomScopes = tokenScopes;
next();
} catch (error) {
if (error.response?.status === 401) {
return res.status(401).json({
error: 'invalid_token',
message: 'Token is invalid or expired'
});
}
next(error);
}
};
}
/**
* Get token information including scopes
*
* IMPORTANT: Scopes are returned during OAuth token exchange, not from API calls.
* You should store the scopes when you receive the access token.
*/
async function getTokenInfo(accessToken) {
// For Server-to-Server OAuth: Decode JWT to get scopes
const parts = accessToken.split('.');
if (parts.length === 3) {
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
return {
scope: payload.scope || '',
exp: payload.exp,
aud: payload.aud
};
}
// For User OAuth tokens: Scopes are NOT available from API responses.
// You must store scopes when you receive them during token exchange.
//
// During OAuth token exchange, the response includes:
// {
// "access_token": "...",
// "token_type": "bearer",
// "scope": "user:read meeting:write ...", <-- Store this!
// "expires_in": 3600
// }
//
// Store the scope in your database alongside the token.
throw new Error(
'User OAuth token scopes must be stored during token exchange. ' +
'Cannot retrieve scopes from an opaque access token.'
);
}
/**
* Example: Store scopes during OAuth token exchange
*/
async function handleOAuthCallback(code) {
const response = await axios.post('https://zoom.us/oauth/token', null, {
params: {
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI
},
auth: {
username: CLIENT_ID,
password: CLIENT_SECRET
}
});
const { access_token, refresh_token, scope, expires_in } = response.data;
// IMPORTANT: Store the scope along with the token
await saveTokenToDatabase({
accessToken: access_token,
refreshToken: refresh_token,
scope: scope, // <-- Store this for later permission checks
expiresAt: Date.now() + (expires_in * 1000)
});
return { access_token, scope };
}
// Usage
const express = require('express');
const app = express();
// Route requiring meeting:read scope
app.get('/api/meetings/:id',
requireScopes(['meeting:read']),
async (req, res) => {
// Token already validated, proceed with API call
const meeting = await getMeeting(req.params.id, req.headers.authorization);
res.json(meeting);
}
);
// Route requiring multiple scopes
app.post('/api/users/:id/meetings',
requireScopes(['user:read', 'meeting:write']),
async (req, res) => {
const meeting = await createMeeting(req.params.id, req.body, req.headers.authorization);
res.json(meeting);
}
);Scope Requirements by Operation
| Operation | User Scope | Admin Scope (S2S) |
|---|---|---|
| Get own user info | user:read | user:read:admin |
| List all users | N/A | user:read:admin |
| Create user | N/A | user:write:admin |
| Get own meetings | meeting:read | meeting:read:admin |
| Get any user's meetings | N/A | meeting:read:admin |
| Create meeting for self | meeting:write | meeting:write:admin |
| Create meeting for others | N/A | meeting:write:admin |
| List own recordings | recording:read | recording:read:admin |
| List any user's recordings | N/A | recording:read:admin |
| Delete own recording | recording:write | recording:write:admin |
| Delete any recording | N/A | recording:write:admin |
| Access own phone | phone:read | phone:read:admin |
| Access any user's phone | N/A | phone:read:admin |
| Manage phone settings | phone:write | phone:write:admin |
Note: "N/A" means this operation requires admin-level scopes and cannot be done with user-level OAuth.
Role-Based Access Control
Define Roles
/**
* Role definitions with allowed scopes
*/
const ROLES = {
admin: {
scopes: [
'user:read:admin', 'user:write:admin',
'meeting:read:admin', 'meeting:write:admin',
'recording:read:admin', 'recording:write:admin',
'account:read:admin', 'account:write:admin'
],
description: 'Full administrative access'
},
manager: {
scopes: [
'user:read:admin',
'meeting:read:admin', 'meeting:write:admin',
'recording:read:admin'
],
description: 'Manage meetings and view users'
},
user: {
scopes: [
'user:read',
'meeting:read', 'meeting:write',
'recording:read'
],
description: 'Manage own meetings and recordings'
},
viewer: {
scopes: [
'meeting:read',
'recording:read'
],
description: 'View-only access'
}
};
/**
* Check if user role has required scope
*/
function roleHasScope(role, requiredScope) {
const roleConfig = ROLES[role];
if (!roleConfig) return false;
return roleConfig.scopes.some(scope => {
// Exact match
if (scope === requiredScope) return true;
// Admin scope covers non-admin version
// e.g., meeting:read:admin covers meeting:read
if (scope.endsWith(':admin')) {
const baseScope = scope.replace(':admin', '');
if (baseScope === requiredScope) return true;
}
return false;
});
}
/**
* Middleware to require a specific role
*/
function requireRole(allowedRoles) {
return (req, res, next) => {
const userRole = req.user?.role; // From your auth system
if (!userRole || !allowedRoles.includes(userRole)) {
return res.status(403).json({
error: 'forbidden',
message: 'Insufficient role permissions',
required_roles: allowedRoles,
your_role: userRole || 'none'
});
}
next();
};
}
// Usage
app.delete('/api/users/:id',
requireRole(['admin']),
requireScopes(['user:write:admin']),
async (req, res) => {
// Only admins can delete users
await deleteUser(req.params.id);
res.json({ success: true });
}
);Permission Checking Between Chained Operations
Chain Validation Pattern
/**
* Validate permissions for a multi-step operation
* before executing any steps
*/
async function validateChainPermissions(operations, tokenScopes) {
const allRequiredScopes = new Set();
for (const op of operations) {
for (const scope of op.requiredScopes) {
allRequiredScopes.add(scope);
}
}
const missingScopes = [...allRequiredScopes].filter(
scope => !tokenScopes.includes(scope)
);
if (missingScopes.length > 0) {
return {
valid: false,
missingScopes,
message: `Cannot complete operation chain. Missing scopes: ${missingScopes.join(', ')}`
};
}
return { valid: true };
}
/**
* Execute a chain of operations with permission validation
*/
async function executeAuthorizedChain(operations, accessToken) {
// Get token scopes
const tokenInfo = await getTokenInfo(accessToken);
const tokenScopes = tokenInfo.scope.split(' ');
// Validate all permissions upfront
const validation = await validateChainPermissions(operations, tokenScopes);
if (!validation.valid) {
throw new Error(validation.message);
}
// Execute operations in sequence
const results = [];
for (const op of operations) {
console.log(`Executing: ${op.name}`);
try {
const result = await op.execute(accessToken, results);
results.push({ name: op.name, success: true, data: result });
} catch (error) {
// Check if it's an authorization error
if (error.response?.status === 403) {
throw new Error(`Authorization failed at step "${op.name}": ${error.response.data.message}`);
}
throw error;
}
}
return results;
}
// Example: User + Meeting creation chain
const userMeetingChain = [
{
name: 'createUser',
requiredScopes: ['user:write:admin'],
execute: async (token, previousResults) => {
return await createUser({
email: 'new@example.com',
firstName: 'New',
lastName: 'User'
}, token);
}
},
{
name: 'createMeeting',
requiredScopes: ['meeting:write:admin'],
execute: async (token, previousResults) => {
const user = previousResults.find(r => r.name === 'createUser').data;
return await createMeeting(user.id, {
topic: 'Onboarding Meeting'
}, token);
}
}
];
// Usage
try {
const results = await executeAuthorizedChain(userMeetingChain, accessToken);
console.log('Chain completed:', results);
} catch (error) {
console.error('Chain failed:', error.message);
}Graceful Degradation
Handle Partial Permissions
/**
* Execute with graceful degradation when permissions are partial
*/
async function executeWithDegradation(operations, accessToken) {
const tokenInfo = await getTokenInfo(accessToken);
const tokenScopes = tokenInfo.scope.split(' ');
const results = [];
for (const op of operations) {
// Check if we have permission for this operation
const hasPermission = op.requiredScopes.every(
scope => tokenScopes.includes(scope)
);
if (!hasPermission) {
if (op.required) {
// Required operation - fail the chain
throw new Error(`Missing required scopes for ${op.name}: ${op.requiredScopes.join(', ')}`);
} else {
// Optional operation - skip with warning
console.warn(`Skipping ${op.name}: insufficient permissions`);
results.push({
name: op.name,
skipped: true,
reason: 'insufficient_permissions',
required_scopes: op.requiredScopes
});
continue;
}
}
// Execute operation
const result = await op.execute(accessToken, results);
results.push({ name: op.name, success: true, data: result });
}
return results;
}
// Example with optional operations
const meetingWithOptionalRecording = [
{
name: 'getMeeting',
required: true,
requiredScopes: ['meeting:read'],
execute: async (token) => getMeetingDetails(meetingId, token)
},
{
name: 'getRecordings',
required: false, // Optional - won't fail chain
requiredScopes: ['recording:read'],
execute: async (token, prev) => {
const meeting = prev.find(r => r.name === 'getMeeting').data;
return getRecordings(meeting.uuid, token);
}
}
];Authorization Decision Flowchart
┌──────────────────────────────────────────────────────────────────────────┐
│ AUTHORIZATION DECISION FLOW │
└──────────────────────────────────────────────────────────────────────────┘
┌─────────────────┐
│ Receive Request │
└────────┬────────┘
│
▼
┌────────────────────────┐
│ Is token present? │
└───────────┬────────────┘
│
┌───────────┴───────────┐
│ NO │ YES
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ Return 401 │ │ Is token valid? │
│ Unauthorized │ └─────────┬──────────┘
└───────────────┘ │
┌───────────┴───────────┐
│ NO │ YES
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ Return 401 │ │ Has required │
│ Invalid Token │ │ scopes? │
└───────────────┘ └─────────┬──────────┘
│
┌───────────┴───────────┐
│ NO │ YES
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ Return 403 │ │ Has resource │
│ Insufficient │ │ access? │
│ Scope │ └─────────┬──────────┘
└───────────────┘ │
┌───────────┴───────────┐
│ NO │ YES
▼ ▼
┌───────────────┐ ┌────────────────┐
│ Return 403 │ │ Execute │
│ Forbidden │ │ Operation │
└───────────────┘ └────────────────┘Common Authorization Errors
| Status | Error | Cause | Solution |
|---|---|---|---|
| 401 | invalid_token | Token expired or revoked | Refresh token or re-authenticate |
| 401 | unauthorized | No token provided | Include Authorization header |
| 403 | insufficient_scope | Token missing required scope | Request additional scopes |
| 403 | forbidden | User lacks resource access | Check user permissions |
| 403 | access_denied | Admin-only operation | Use admin account |
Best Practices
1. Validate upfront - Check all permissions before starting a chain 2. Fail fast - Return clear error messages with required scopes 3. Graceful degradation - Skip optional steps rather than fail entirely 4. Audit logging - Log all authorization decisions 5. Principle of least privilege - Request only needed scopes 6. Token caching - Cache token info to avoid repeated validation calls
Real-World Examples
See these use-cases for authorization patterns in action:
- [User + Meeting Creation](../use-cases/user-and-meeting-creation.md) - Multi-step provisioning with scope validation
- [Meeting Details with Events](../use-cases/meeting-details-with-events.md) - REST API + webhooks with permission checking
- [Meeting Automation](../use-cases/meeting-automation.md) - Meeting management with admin scope requirements
Resources
- OAuth Scopes Reference: https://developers.zoom.us/docs/integrations/oauth-scopes/
- API Error Codes: https://developers.zoom.us/docs/api/rest/error-handling/
- Authentication Guide: authentication.md
- Scopes Reference: scopes.md
Automatic Skill Chaining: REST API + Webhooks
This guide provides executable patterns for handling a multi-faceted workflow that needs both:
- synchronous REST API operations (
zoom-rest-api) - asynchronous event processing (
zoom-webhooks)
Chain Selection Logic
export type SkillChain = {
selectedSkills: string[];
executionOrder: string[];
};
export function chooseRestWebhookChain(query: string): SkillChain {
const q = query.toLowerCase();
const needsRest = /create meeting|update meeting|list users|rest api|\/v2\//.test(q);
const needsWebhook = /webhook|event|meeting\.started|participant|real-time update/.test(q);
const selectedSkills = ['zoom-general'];
if (needsRest || needsWebhook) selectedSkills.push('zoom-oauth');
if (needsRest) selectedSkills.push('zoom-rest-api');
if (needsWebhook) selectedSkills.push('zoom-webhooks');
return {
selectedSkills,
executionOrder: selectedSkills,
};
}Reference Architecture
Client/API Caller
-> Orchestrator API
-> OAuth token manager
-> REST API worker (create/update meetings)
-> Persistence (meeting state + idempotency keys)
<- immediate REST result
Zoom Event Pipeline
Zoom -> Webhook ingress (signature verify + URL validation)
-> Queue
-> Event processors
-> State projection / downstream notificationsMinimal Runnable Example (Node.js)
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json({
verify: (req, _res, buf) => {
req.rawBody = buf.toString('utf8');
},
}));
const tokenCache = { accessToken: '', expiresAt: 0 };
const meetingStore = new Map();
async function getAccessToken() {
const now = Date.now();
if (tokenCache.accessToken && now < tokenCache.expiresAt - 60_000) {
return tokenCache.accessToken;
}
const params = new URLSearchParams({
grant_type: 'account_credentials',
account_id: process.env.ZOOM_ACCOUNT_ID,
});
const basic = Buffer.from(`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`).toString('base64');
const res = await fetch(`https://zoom.us/oauth/token?${params}`, {
method: 'POST',
headers: { Authorization: `Basic ${basic}` },
});
if (!res.ok) throw new Error(`token_exchange_failed:${res.status}`);
const data = await res.json();
tokenCache.accessToken = data.access_token;
tokenCache.expiresAt = now + data.expires_in * 1000;
return tokenCache.accessToken;
}
app.post('/api/meetings', async (req, res) => {
try {
const token = await getAccessToken();
const hostUserId = process.env.ZOOM_HOST_USER_ID;
if (!hostUserId) {
return res.status(500).json({ error: 'missing_host_user_id', detail: 'Set ZOOM_HOST_USER_ID for S2S meeting creation' });
}
const body = {
topic: req.body.topic || 'Auto Meeting',
type: 2,
start_time: req.body.start_time,
duration: req.body.duration || 30,
timezone: req.body.timezone || 'UTC',
};
const z = await fetch(`https://api.zoom.us/v2/users/${encodeURIComponent(hostUserId)}/meetings`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await z.json();
if (!z.ok) return res.status(z.status).json(data);
meetingStore.set(String(data.id), { status: 'scheduled', topic: data.topic, participants: 0 });
return res.status(201).json(data);
} catch (err) {
return res.status(500).json({ error: 'create_meeting_failed', detail: String(err) });
}
});
function verifySignature(req) {
const ts = req.headers['x-zm-request-timestamp'];
const sig = req.headers['x-zm-signature'];
const msg = `v0:${ts}:${req.rawBody || ''}`;
const expected = `v0=${crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET).update(msg).digest('hex')}`;
return sig === expected;
}
app.post('/webhooks/zoom', (req, res) => {
if (req.body.event === 'endpoint.url_validation') {
const plainToken = req.body.payload?.plainToken;
const encryptedToken = crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET).update(plainToken).digest('hex');
return res.json({ plainToken, encryptedToken });
}
if (!verifySignature(req)) return res.status(401).send('invalid_signature');
const evt = req.body.event;
const id = String(req.body.payload?.object?.id || '');
if (id && !meetingStore.has(id)) meetingStore.set(id, { status: 'unknown', participants: 0 });
const state = meetingStore.get(id);
if (state) {
if (evt === 'meeting.started') state.status = 'in_progress';
if (evt === 'meeting.ended') state.status = 'ended';
if (evt === 'meeting.participant_joined') state.participants += 1;
if (evt === 'meeting.participant_left') state.participants = Math.max(0, state.participants - 1);
}
return res.status(200).send('ok');
});
app.listen(process.env.PORT || 3001, () => {
console.log('orchestrator listening');
});Failure Handling Minimums
- REST call failures: retry with jitter for
429/5xx; do not retry4xxbusiness errors blindly. - Webhook ingestion: always return
200after durable enqueue or local persistence. - Idempotency: dedupe by
event_idor (event,event_ts,meeting_uuid) composite key. - Reconciliation: periodic REST poll to repair missed webhook events.
Environment Variables
ZOOM_ACCOUNT_IDZOOM_CLIENT_IDZOOM_CLIENT_SECRETZOOM_HOST_USER_ID(required for S2S meeting creation; do not rely onme)ZOOM_WEBHOOK_SECRETPORT
Official Zoom Sample Repositories
Curated list of official repositories from Zoom for development. Organized by product/SDK.
---
Meeting SDK
Official Samples (by Zoom)
| Repository | Stars | Description |
|---|---|---|
| meetingsdk-web-sample | 643 | Web SDK sample - Component View and Client View |
| meetingsdk-web | 324 | NPM package for embedding meetings |
| meetingsdk-react-sample | 177 | React integration sample |
| meetingsdk-auth-endpoint-sample | 124 | Generate Meeting SDK JWT signatures |
| meetingsdk-angular-sample | 60 | Angular integration sample |
| meetingsdk-vuejs-sample | 42 | Vue.js integration sample |
| meetingsdk-javascript-sample | 41 | Vanilla JavaScript sample |
| meetingsdk-headless-linux-sample | 3 | Headless Linux bot with Docker |
---
Video SDK
Official Samples (by Zoom)
| Repository | Stars | Description |
|---|---|---|
| videosdk-web-sample | 137 | Web Video SDK sample |
| videosdk-web | 56 | NPM package for custom video |
| videosdk-auth-endpoint-sample | 23 | Generate Video SDK JWT signatures |
| videosdk-zoom-ui-toolkit-web | 17 | Prebuilt video chat UI |
| videosdk-zoom-ui-toolkit-react-sample | 17 | UI Toolkit in React |
| videosdk-nextjs-quickstart | 16 | Next.js integration |
| videosdk-zoom-ui-toolkit-javascript-sample | 11 | UI Toolkit in vanilla JS |
| VideoSDK-Web-Telehealth | 11 | Telehealth starter kit |
| videosdk-workshop | 9 | Workshop project |
| videosdk-s3-cloud-recordings | 8 | Auto-upload recordings to S3 |
| videosdk-web-helloworld | 4 | Minimal hello world |
| videosdk-zoom-ui-toolkit-angular-sample | 4 | UI Toolkit in Angular |
| videosdk-zoom-ui-toolkit-vuejs-sample | 3 | UI Toolkit in Vue.js |
| videosdk-vue-nuxt-quickstart | 1 | Vue/Nuxt quickstart |
| videosdk-electron-sample | 1 | Electron sample |
| videosdk-linux-raw-recording-sample | - | Linux headless raw data capture |
---
REST API
Official Samples (by Zoom)
| Repository | Stars | Description |
|---|---|---|
| oauth-sample-app | 91 | Node.js OAuth sample |
| server-to-server-oauth-starter-api | 54 | S2S OAuth starter API |
| api | 44 | API v2 documentation |
| user-level-oauth-starter | 27 | User-level OAuth starter |
| server-to-server-oauth-token | 15 | S2S token generation utility |
| rivet-javascript | 13 | Rivet API library (auth + webhooks + API) |
| websocket-js-sample | 5 | WebSocket connection demo |
| websocket-redis-example | 4 | WebSocket with Redis |
| server-to-server-python-sample | 4 | Python S2S OAuth sample |
| task-manager-sample | 3 | Unified build flow showcase |
| rivet-javascript-sample | 3 | Rivet standup bot sample |
| sample-registration-app | 3 | Webinar registration with rate limits |
---
Webhooks
Official Samples (by Zoom)
| Repository | Stars | Description |
|---|---|---|
| webhook-sample | 34 | Receive Zoom webhooks (Node.js) |
| zoom-webhook-verification-headers | - | Custom header auth + webhook validation |
| webhook-to-postgres | 5 | Store webhooks in PostgreSQL |
| Go-Webhooks | - | Go/Fiber webhook listener |
---
Zoom Apps SDK
Official Samples (by Zoom)
| Repository | Stars | Description |
|---|---|---|
| zoomapps-sample-js | 66 | Hello World Zoom App (vanilla JS) |
| zoomapps-advancedsample-react | 55 | Advanced React sample |
| appssdk | 49 | Zoom Apps SDK NPM package |
| zoomapps-texteditor-vuejs | 16 | Collaborate Mode text editor |
| zoomapps-customlayout-js | 16 | Immersive Mode / Layers API |
| zoomapps-workshop-sample | 6 | Getting started workshop |
| zoomapps-serverless-vuejs | 6 | Serverless on Firebase |
| zoomapps-cameramode-vuejs | 6 | Camera Mode + Immersive Mode |
| arlo-meeting-assistant | 2 | RTMS-powered meeting assistant |
| meetingbot-recall-sample | 2 | Meeting bot with Recall.ai + Claude |
---
RTMS (Real-Time Media Streams)
Official Samples (by Zoom)
| Repository | Stars | Description |
|---|---|---|
| zoom-rtms | 29 | Cross-platform RTMS wrapper (Node.js, Python, Go) |
| rtms-samples | 22 | Official RTMS sample apps |
| rtms-developer-preview-js | 3 | Developer preview hello world |
| rtms-sdk-cpp | 2 | C++ RTMS SDK (librtmsdk) |
| rtms-meeting-assistant-starter-kit | 1 | Meeting assistant starter kit |
| rtms-quickstart-js | 1 | Node.js quickstart |
| zoom_rtms_langchain_sample | 1 | LangChain + transcripts for action items |
---
Team Chat & Chatbots
Official Samples (by Zoom)
| Repository | Stars | Description |
|---|---|---|
| unsplash-chatbot | 19 | Send Unsplash photos in Team Chat |
| node.js-chatbot | 18 | Node.js chatbot library |
| vote-chatbot | 10 | Voting bot for Team Chat |
| catbot | 9 | Cat photo bot |
| node.js-chatbot-cli | 8 | Chatbot CLI tool |
| zoom-chatbot-claude-sample | 6 | Anthropic Claude in Team Chat |
| Zoom-Chat-Neural-Search-Assistant-Sample | 2 | Cerebras + Exa search bot |
| zoom-team-chat-shortcut-sample | 1 | Recording management shortcut |
| zoom-teams-chat-snowflake-sample | 1 | Snowflake + Cortex integration |
| zoom-erp-chatbot-sample | 1 | Oracle ERP integration |
| chatbot-nodejs-quickstart | - | Node.js chatbot quickstart |
| chatbot-python-sample | - | Python chatbot with threading |
---
Cobrowse SDK
Official Samples (by Zoom)
| Repository | Stars | Description |
|---|---|---|
| CobrowseSDK-Quickstart | 1 | Cobrowse SDK quickstart |
| cobrowsesdk-auth-endpoint-sample | 2 | JWT generation for Cobrowse |
---
Tooling & Utilities
Official Tools (by Zoom)
| Repository | Stars | Description |
|---|---|---|
| probesdk-web | 3 | Test device/network/server connection |
---
Distributed Meeting Creation and Event Processing with Fallbacks
Use this architecture for high-volume meeting creation with resilient event processing.
Core Architectural Considerations
1. Separation of planes
- Command plane: REST meeting creation/update APIs.
- Event plane: webhook ingestion and async projection.
2. Idempotency and dedupe
- Require caller-provided idempotency key per create request.
- Dedupe webhook events by stable event key.
3. Token isolation
- Central token broker with distributed lock (Redis/Postgres advisory lock).
4. Backpressure and queueing
- Queue all webhook events and meeting commands.
- Use DLQ for poison messages.
5. Fallback mechanisms
- Retry with exponential backoff + jitter for retriable failures (
429/5xx/network). - Circuit breaker around Zoom API dependency.
- Reconciliation poller when webhook delivery is delayed/missed.
Reference Topology
API Gateway
-> Meeting Command Service
-> Idempotency Store (Redis/Postgres)
-> Token Broker
-> Zoom REST API
-> Outbox/Event Bus
Webhook Ingress
-> Signature Verify + URL Validation
-> Queue (Kafka/SQS/Rabbit)
-> Projection Workers
-> Meeting State Store
Recovery Services
-> Retry Worker
-> Reconciliation Poller (REST pull)
-> Dead Letter ReprocessorCommand Plane Example (Meeting Creation Service)
type CreateMeetingInput = {
idempotencyKey: string;
hostUserId: string; // explicit user for S2S
topic: string;
startTime: string;
duration: number;
};
type QueuePublisher = { publish: (topic: string, payload: object) => Promise<void> };
type IdempotencyStore = {
get: (key: string) => Promise<object | null>;
put: (key: string, value: object, ttlSec: number) => Promise<void>;
};
export async function createMeetingCommand(
input: CreateMeetingInput,
deps: {
tokenBroker: { getToken: () => Promise<string> };
idempotency: IdempotencyStore;
queue: QueuePublisher;
breaker: CircuitBreaker;
},
) {
const cached = await deps.idempotency.get(input.idempotencyKey);
if (cached) return cached;
if (!deps.breaker.canCall()) {
// degraded mode: queue command for delayed processing
await deps.queue.publish('meeting.create.delayed', input);
return { accepted: true, mode: 'degraded_queued' };
}
const op = async () => {
const token = await deps.tokenBroker.getToken();
const res = await fetch(
`https://api.zoom.us/v2/users/${encodeURIComponent(input.hostUserId)}/meetings`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
topic: input.topic,
type: 2,
start_time: input.startTime,
duration: input.duration,
}),
},
);
if (!res.ok) {
const err = new Error(`zoom_create_failed:${res.status}`);
(err as any).status = res.status;
throw err;
}
return res.json();
};
try {
const created = await retry(
op,
{ retries: 4, baseMs: 300, maxMs: 5000 },
(e) => [429, 500, 502, 503, 504].includes((e as any).status),
);
deps.breaker.recordSuccess();
await deps.idempotency.put(input.idempotencyKey, created, 3600);
await deps.queue.publish('meeting.created', { meetingId: created.id, hostUserId: input.hostUserId });
return created;
} catch (e) {
deps.breaker.recordFailure();
throw e;
}
}Event Plane Example (Webhook Ingress + Queue + Projection)
import crypto from 'crypto';
export function verifyWebhook(rawBody: string, ts: string, sig: string, secret: string): boolean {
// reject stale requests to reduce replay risk
const nowSec = Math.floor(Date.now() / 1000);
const tsSec = Number(ts || 0);
if (!Number.isFinite(tsSec) || Math.abs(nowSec - tsSec) > 300) return false;
const msg = `v0:${ts}:${rawBody}`;
const expected = `v0=${crypto.createHmac('sha256', secret).update(msg).digest('hex')}`;
return sig === expected;
}
export async function ingestWebhook(req: any, res: any, queue: QueuePublisher, secret: string) {
if (req.body.event === 'endpoint.url_validation') {
const plainToken = req.body.payload?.plainToken;
const encryptedToken = crypto.createHmac('sha256', secret).update(plainToken).digest('hex');
return res.json({ plainToken, encryptedToken });
}
const ts = String(req.headers['x-zm-request-timestamp'] || '');
const sig = String(req.headers['x-zm-signature'] || '');
const raw = String(req.rawBody || '');
if (!verifyWebhook(raw, ts, sig, secret)) return res.status(401).send('invalid_signature');
try {
// durable write first, then ack
await queue.publish('zoom.webhook.raw', req.body);
return res.status(200).send('ok');
} catch {
// non-200 triggers Zoom retry for at-least-once delivery
return res.status(503).send('queue_unavailable');
}
}
export async function projectEvent(evt: any, stateStore: any, dedupe: IdempotencyStore) {
const dedupeKey = `${evt.event}:${evt.event_ts}:${evt.payload?.object?.uuid || evt.payload?.object?.id || 'unknown'}`;
const seen = await dedupe.get(dedupeKey);
if (seen) return;
const id = String(evt.payload?.object?.id || '');
const current = (await stateStore.get(id)) || { status: 'unknown', participants: 0, lastEventTs: 0 };
if (evt.event_ts < current.lastEventTs) {
await dedupe.put(dedupeKey, { stale: true }, 86400);
return;
} // stale event guard
if (evt.event === 'meeting.started') current.status = 'in_progress';
if (evt.event === 'meeting.ended') current.status = 'ended';
if (evt.event === 'meeting.participant_joined') current.participants += 1;
if (evt.event === 'meeting.participant_left') current.participants = Math.max(0, current.participants - 1);
current.lastEventTs = evt.event_ts;
await stateStore.put(id, current);
await dedupe.put(dedupeKey, { ok: true }, 86400);
}Express raw-body setup (required for signature verification)
app.use(express.json({
verify: (req: any, _res, buf) => {
req.rawBody = buf.toString('utf8');
},
}));Retry + Circuit Breaker Example (TypeScript)
type RetryOptions = {
retries: number;
baseMs: number;
maxMs: number;
};
function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
function backoff(attempt: number, baseMs: number, maxMs: number) {
const exp = Math.min(maxMs, baseMs * 2 ** attempt);
const jitter = Math.floor(Math.random() * Math.min(250, exp / 4));
return exp + jitter;
}
export async function retry<T>(fn: () => Promise<T>, opts: RetryOptions, isRetriable: (e: any) => boolean): Promise<T> {
let lastErr: any;
for (let i = 0; i <= opts.retries; i += 1) {
try {
return await fn();
} catch (e) {
lastErr = e;
if (i === opts.retries || !isRetriable(e)) break;
await sleep(backoff(i, opts.baseMs, opts.maxMs));
}
}
throw lastErr;
}
export class CircuitBreaker {
private failures = 0;
private openUntil = 0;
constructor(private threshold = 5, private coolDownMs = 15_000) {}
canCall() {
return Date.now() > this.openUntil;
}
recordSuccess() {
this.failures = 0;
}
recordFailure() {
this.failures += 1;
if (this.failures >= this.threshold) {
this.openUntil = Date.now() + this.coolDownMs;
}
}
}Reconciliation Poller Example (Fallback for Missed Events)
export async function reconcileMeetingState(
meetingId: string,
hostUserId: string,
deps: {
tokenBroker: { getToken: () => Promise<string> };
stateStore: { get: (id: string) => Promise<any>; put: (id: string, v: any) => Promise<void> };
},
) {
const token = await deps.tokenBroker.getToken();
const res = await fetch(`https://api.zoom.us/v2/meetings/${encodeURIComponent(meetingId)}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) return;
const apiState = await res.json();
const projected = (await deps.stateStore.get(meetingId)) || {};
const merged = {
...projected,
status: apiState.status || projected.status,
topic: apiState.topic || projected.topic,
hostId: hostUserId,
reconciledAt: Date.now(),
};
await deps.stateStore.put(meetingId, merged);
}Distributed Coordination and Load-Balancing Considerations
- Partition command/event streams by
meetingIdorhostUserIdso all updates for one meeting land on the same consumer shard. - Use a distributed lock for shared singleton jobs (token refresh rotation, reconciliation scheduler leader).
- Keep webhook ingress stateless so horizontal autoscaling is safe behind L4/L7 load balancers.
- Apply queue consumer concurrency limits to protect downstream Zoom API quotas.
Redis-Style Lock Skeleton
export async function withLock(lock: { acquire: (k: string, ttlMs: number) => Promise<boolean>; release: (k: string) => Promise<void> }, key: string, fn: () => Promise<void>) {
const got = await lock.acquire(key, 10_000);
if (!got) return;
try {
await fn();
} finally {
await lock.release(key);
}
}Token Broker Example (Cached Refresh + Distributed Lock)
type CachedToken = { accessToken: string; expiresAtMs: number };
export class TokenBroker {
constructor(
private cache: { get: (k: string) => Promise<CachedToken | null>; put: (k: string, v: CachedToken, ttlSec: number) => Promise<void> },
private lock: { acquire: (k: string, ttlMs: number) => Promise<boolean>; release: (k: string) => Promise<void> },
private fetchToken: () => Promise<{ access_token: string; expires_in: number }>,
) {}
async getToken(): Promise<string> {
const cached = await this.cache.get('zoom:s2s-token');
const now = Date.now();
if (cached && cached.expiresAtMs - now > 60_000) {
return cached.accessToken;
}
const gotLock = await this.lock.acquire('zoom:s2s-token:refresh', 10_000);
if (!gotLock) {
await sleep(200);
const retryCached = await this.cache.get('zoom:s2s-token');
if (retryCached && retryCached.expiresAtMs - Date.now() > 30_000) {
return retryCached.accessToken;
}
throw new Error('token_refresh_lock_contention');
}
try {
const fresh = await this.fetchToken();
const value = {
accessToken: fresh.access_token,
expiresAtMs: Date.now() + fresh.expires_in * 1000,
};
await this.cache.put('zoom:s2s-token', value, Math.max(60, fresh.expires_in - 90));
return value.accessToken;
} finally {
await this.lock.release('zoom:s2s-token:refresh');
}
}
}High-Volume Create Worker (Concurrency + Rate Protection)
type CreateJob = CreateMeetingInput & { attempts: number };
class TokenBucket {
private tokens: number;
private lastRefill = Date.now();
constructor(private readonly capacity: number, private readonly refillPerSec: number) {
this.tokens = capacity;
}
async take() {
while (true) {
const now = Date.now();
const elapsedSec = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillPerSec);
this.lastRefill = now;
if (this.tokens >= 1) {
this.tokens -= 1;
return;
}
await sleep(100);
}
}
}
export async function runCreateWorker(
queue: { receiveBatch: (n: number) => Promise<CreateJob[]>; ack: (job: CreateJob) => Promise<void>; retryLater: (job: CreateJob, delayMs: number) => Promise<void> },
deps: {
createMeeting: (job: CreateJob) => Promise<void>;
breaker: CircuitBreaker;
limiter: TokenBucket;
},
concurrency = 8,
) {
while (true) {
const jobs = await queue.receiveBatch(concurrency);
await Promise.all(jobs.map(async (job) => {
if (!deps.breaker.canCall()) {
await queue.retryLater(job, 30_000);
return;
}
try {
await deps.limiter.take();
await deps.createMeeting(job);
deps.breaker.recordSuccess();
await queue.ack(job);
} catch (e: any) {
deps.breaker.recordFailure();
const delay = backoff(job.attempts, 500, 60_000);
await queue.retryLater({ ...job, attempts: job.attempts + 1 }, delay);
}
}));
}
}Reconciliation Scheduler (Lag Detection + Leader Election)
export async function reconcileLaggingMeetings(
deps: {
lock: { acquire: (k: string, ttlMs: number) => Promise<boolean>; release: (k: string) => Promise<void> };
stateStore: { listLagging: (ageMs: number, limit: number) => Promise<Array<{ meetingId: string; hostUserId: string }>> };
reconcile: (meetingId: string, hostUserId: string) => Promise<void>;
},
) {
await withLock(deps.lock, 'zoom:reconcile:leader', async () => {
const lagging = await deps.stateStore.listLagging(5 * 60_000, 250);
for (const item of lagging) {
await deps.reconcile(item.meetingId, item.hostUserId);
}
});
}DLQ Replay Worker
export async function replayDlq(
dlq: { receiveBatch: (n: number) => Promise<any[]>; ack: (msg: any) => Promise<void>; moveBack: (topic: string, msg: any) => Promise<void> },
topic = 'meeting.create.delayed',
) {
const failed = await dlq.receiveBatch(100);
for (const msg of failed) {
await dlq.moveBack(topic, { ...msg, replayedAt: Date.now() });
await dlq.ack(msg);
}
}Distributed State Rules
- Meeting state is event-sourced or projection-based, not only request-response based.
- Persist
last_seen_event_tsand status transitions to handle out-of-order events. - Add monotonic transition guards (e.g., do not move
ended -> in_progress).
Fallback Matrix
| Failure | Primary response | Fallback |
|---|---|---|
| Token refresh failure | Retry token exchange | Fail fast + alert + pause new create requests |
REST 429 / 5xx | Retry w/ backoff | Queue command for delayed retry |
| Webhook verification failure | Reject 401 | Alert security pipeline |
| Webhook processor down | Buffer in queue | DLQ + replay job |
| Missing webhook event | Detect via reconciliation lag | REST poll and repair projection |
| Dependency outage | Open circuit breaker | Serve degraded status + queued commands |
Cross-Product Environment Variables (Hub)
Use this file as a normalization map. Product-specific details are maintained in each product skill reference.
Common .env keys
| Variable | Typical products | Where to find |
|---|---|---|
ZOOM_CLIENT_ID | OAuth, REST API, Team Chat, WebSockets, RTMS (OAuth mode), Contact Center APIs | Zoom Marketplace -> your app -> App Credentials |
ZOOM_CLIENT_SECRET | OAuth, REST API, Team Chat, WebSockets, RTMS (OAuth mode), Contact Center APIs | Zoom Marketplace -> your app -> App Credentials |
ZOOM_ACCOUNT_ID | Server-to-Server OAuth flows | Zoom Marketplace -> Server-to-Server OAuth app credentials |
ZOOM_REDIRECT_URI | User-level OAuth apps | Zoom Marketplace -> OAuth redirect/allow list |
ZOOM_WEBHOOK_SECRET / WEBHOOK_SECRET_TOKEN | Webhooks and event validation | Zoom Marketplace -> Event Subscriptions -> Secret Token |
ZOOM_SDK_KEY / ZOOM_SDK_SECRET | Meeting SDK or SDK-based products | Zoom Marketplace -> SDK app credentials |
ZOOM_VIDEO_SDK_KEY / ZOOM_VIDEO_SDK_SECRET | Video SDK and UI Toolkit | Zoom Marketplace -> Video SDK app credentials |
PROBE_JS_URL / PROBE_WASM_URL | Probe SDK | Your app/CDN hosted Probe SDK assets (or bundler output paths) |
PROBE_DOMAIN / PROBE_CONNECT_TIMEOUT_MS | Probe SDK | Product policy + Probe SDK diagnostics configuration |
Product references
- ../../zoom-apps-sdk/references/environment-variables.md
- ../../cobrowse-sdk/references/environment-variables.md
- ../../meeting-sdk/references/environment-variables.md
- ../../oauth/references/environment-variables.md
- ../../rest-api/references/environment-variables.md
- ../../rtms/references/environment-variables.md
- ../../team-chat/references/environment-variables.md
- ../../ui-toolkit/references/environment-variables.md
- ../../video-sdk/references/environment-variables.md
- ../../webhooks/references/environment-variables.md
- ../../websockets/references/environment-variables.md
- ../../contact-center/references/environment-variables.md
- ../../phone/references/environment-variables.md
- ../../probe-sdk/references/environment-variables.md
Probe SDK note
- Probe SDK core diagnostics do not require Zoom OAuth/Marketplace credentials.
Interview Answer: Routing with zoom-general
Use zoom-general as the triage layer, then route implementation to specialized skills.
Short answer
1. Classify the query in zoom-general by product intent, platform, and integration pattern. 2. Route to the minimum specialized skills:
- Auth/scopes ->
zoom-oauth - API operations ->
zoom-rest-api - Embedded meetings ->
zoom-meeting-sdk - Custom video experiences ->
zoom-video-sdk - Event delivery ->
zoom-webhooksorzoom-websockets - Live media/transcripts ->
zoom-rtms
3. Execute in sequence: zoom-general -> auth -> core product -> events/media. 4. If ambiguous, ask one disambiguation question before locking the chain.
Canonical guidance and handoff structure:
- Query Routing Playbook
Known Limitations & Quirks
Common gotchas and limitations developers encounter.
Recording Limitations
Minimum Recording Duration
Recordings shorter than 3-5 seconds will NOT be saved.
This applies to:
- Cloud recordings
- Local recordings via SDK
If you need to capture very short sessions, ensure the recording runs for at least 5 seconds.
API Limitations
Rate Limits
See Rate Limits for detailed information.
Key points:
- Create/update meeting endpoints are Heavy (stricter limits)
- Response headers show remaining quota
- Implement exponential backoff for 429 errors
Error Code 0
The enum value 0 often represents SUCCESS, not failure.
Always check the SDK enum values:
// Example: Meeting SDK
SDKERR_SUCCESS = 0 // This is success!
SDKERR_UNKNOWN = 1 // This is an errorDon't assume 0 = error in your error handling.
Video SDK Web Limitations
Video Rendering Performance
Use ONE rendering control for all videos, not one per participant.
Multiple rendering controls severely degrade performance. See Video SDK Web.
SharedArrayBuffer
Some features require SharedArrayBuffer headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpAs of v1.11.2, this is elective for basic functionality.
SDK Signature Limitations
Minimum Token Validity
Zoom may require exp - iat >= 2 hours.
Workaround: Set iat in the past:
const iat = Math.floor(Date.now() / 1000) - 7200; // 2 hours ago
const exp = Math.floor(Date.now() / 1000) + 10; // 10 seconds from nowThis gives you a short-lived token while satisfying the validity requirement.
SDK Download
Marketplace Sign-in Required
Meeting SDK and Video SDK (except Web npm packages) must be downloaded from Marketplace after signing in.
They are not available on public package managers for native platforms.
Platform-Specific
iOS
- Requires camera/microphone entitlements
- Background audio requires special configuration
Android
- Requires runtime permissions for camera/mic
- ProGuard rules may be needed
Linux
- Headless operation requires X virtual framebuffer (Xvfb) for some features
- Limited UI customization compared to other platforms
Resources
- Developer forum: https://devforum.zoom.us/ (search for known issues)
- Support: https://devsupport.zoom.us/
Zoom App Marketplace
Navigate the Zoom Marketplace developer portal.
Overview
The Zoom App Marketplace is where you create, configure, and publish Zoom apps.
Getting Started
1. Go to marketplace.zoom.us 2. Sign in with your Zoom account 3. Click Develop → Build App 4. Choose app type 5. Configure app settings
Portal Sections
Develop
- Build App - Create new apps
- Manage - Edit existing apps
- Logs - View API and webhook logs
App Configuration
| Section | Purpose |
|---|---|
| App Credentials | SDK Key/Secret, Client ID/Secret |
| Scopes | Configure OAuth permissions |
| Feature | Enable Meeting SDK, Video SDK, Webhooks |
| Activation | Make app installable |
SDK Downloads
Important: Meeting SDK and Video SDK must be downloaded from Marketplace after signing in. They are not available on public package managers (except Web SDKs via npm).
1. Go to your app's Download section 2. Select platform (iOS, Android, Windows, macOS, Linux) 3. Download SDK package
Credentials
OAuth Apps
- Client ID - Public identifier
- Client Secret - Keep secret, server-side only
SDK Apps
- SDK Key - Used in JWT payload
- SDK Secret - Used to sign JWT, keep secret
Publishing
To publish to Marketplace:
1. Complete app configuration 2. Submit for review 3. Address feedback 4. Get approved 5. Go live
Resources
- Marketplace: https://marketplace.zoom.us/
- Developer docs: https://developers.zoom.us/
Meeting + Webhooks + OAuth Refresh Orchestration
This guide implements one solution that handles all three simultaneously: 1. create meeting, 2. process webhook updates, 3. refresh OAuth tokens safely.
Direct Answer
Use this skill chain:
1. zoom-general to classify the request 2. zoom-oauth for token brokerage and refresh control 3. zoom-rest-api to create the meeting 4. zoom-webhooks to receive real-time updates
Minimal flow:
client request
-> TokenBroker.getToken()
-> POST /v2/users/{userId}/meetings
-> persist meeting + idempotency key
-> Zoom sends webhooks to your ingress
-> verify signature
-> enqueue event
-> projection worker updates meeting stateWebhook subscription note:
- the receiver implementation lives in your app code
- the actual Zoom event subscription is configured at the Marketplace app level
- do not model webhook subscription enablement as a per-request runtime API step unless Zoom exposes a product-specific admin API for that exact surface
Skill Chain
1. zoom-general 2. zoom-oauth 3. zoom-rest-api 4. zoom-webhooks
Component Design
TokenBroker: central access token cache + refresh lock.MeetingService: REST calls using broker.WebhookIngress: signature validation + URL validation + event enqueue.ProjectionWorker: applies events to meeting state.
Token Broker with Refresh Lock (TypeScript)
type TokenState = { accessToken: string; expiresAt: number; refreshing?: Promise<string> };
export class TokenBroker {
private state: TokenState = { accessToken: '', expiresAt: 0 };
constructor(
private accountId: string,
private clientId: string,
private clientSecret: string,
) {}
async getToken(): Promise<string> {
const now = Date.now();
if (this.state.accessToken && now < this.state.expiresAt - 60_000) {
return this.state.accessToken;
}
if (!this.state.refreshing) {
this.state.refreshing = this.refresh();
this.state.refreshing.finally(() => { this.state.refreshing = undefined; });
}
return this.state.refreshing;
}
invalidate() {
this.state.accessToken = '';
this.state.expiresAt = 0;
}
async forceRefresh(): Promise<string> {
this.invalidate();
return this.getToken();
}
private async refresh(): Promise<string> {
const q = new URLSearchParams({ grant_type: 'account_credentials', account_id: this.accountId });
const basic = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
const res = await fetch(`https://zoom.us/oauth/token?${q.toString()}`, {
method: 'POST',
headers: { Authorization: `Basic ${basic}` },
});
if (!res.ok) throw new Error(`token_refresh_failed:${res.status}`);
const data = await res.json() as { access_token: string; expires_in: number };
this.state.accessToken = data.access_token;
this.state.expiresAt = Date.now() + data.expires_in * 1000;
return this.state.accessToken;
}
}Meeting Service with 401 Retry-once
export async function createMeeting(tokenBroker: TokenBroker, userId: string, payload: object) {
async function call(): Promise<Response> {
const token = await tokenBroker.getToken();
return fetch(`https://api.zoom.us/v2/users/${encodeURIComponent(userId)}/meetings`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
}
let res = await call();
if (res.status === 401) {
await tokenBroker.forceRefresh();
res = await call(); // retry once with fresh token
}
if (!res.ok) throw new Error(`create_meeting_failed:${res.status}`);
return res.json();
}Webhook Ingress Skeleton
import crypto from 'crypto';
import type { Request, Response } from 'express';
export function verifyZoomSignature(req: Request, secret: string): boolean {
const ts = String(req.headers['x-zm-request-timestamp'] || '');
const sig = String(req.headers['x-zm-signature'] || '');
const rawBody = (req as any).rawBody || JSON.stringify(req.body);
const msg = `v0:${ts}:${rawBody}`;
const expected = `v0=${crypto.createHmac('sha256', secret).update(msg).digest('hex')}`;
return sig === expected;
}
export async function handleWebhook(req: Request, res: Response, secret: string, enqueue: (e: any) => Promise<void>) {
if (req.body?.event === 'endpoint.url_validation') {
const plainToken = req.body.payload?.plainToken;
const encryptedToken = crypto.createHmac('sha256', secret).update(plainToken).digest('hex');
return res.json({ plainToken, encryptedToken });
}
if (!verifyZoomSignature(req, secret)) {
return res.status(401).send('invalid_signature');
}
await enqueue(req.body); // durable queue write
return res.status(200).send('ok');
}Event Processing Rules
- Apply idempotency key to avoid duplicate state updates.
- Accept out-of-order events; keep
last_event_tsand reject stale writes when necessary. - Add reconciliation worker that polls REST meeting status if webhook lag or failures are detected.
Runtime Setup Notes
- For Server-to-Server OAuth meeting creation, pass an explicit host
userId/email instead of relying onme. - In Express, capture raw request body in
express.json({ verify })and use it for signature verification.
Query Routing Playbook (zoom-general)
Use zoom-general as the routing/orchestration layer. Do not implement product-specific logic in zoom-general if a specialized skill exists.
Goal
Convert a complex developer query into:
selected_skillsexecution_orderassumptionsnext_actions
Routing rules
| Query signal | Route to skill | Why |
|---|---|---|
| OAuth, scopes, S2S, token strategy | zoom-oauth | Authentication and authorization design |
| Meetings/users/recordings/reports API operations | zoom-rest-api | Server-side Zoom resource management |
| Embed full Zoom meetings/webinars | zoom-meeting-sdk | Meeting runtime integration |
| Build custom video session experience | zoom-video-sdk | Custom media UX runtime |
| Receive event callbacks via HTTP | zoom-webhooks | Event lifecycle notifications |
| Need lower-latency event stream | zoom-websockets | Persistent real-time event transport |
| Live audio/video/transcript stream ingestion | zoom-rtms | Real-time media and transcript pipeline |
| App runs inside Zoom client | zoom-apps-sdk | In-client app model and APIs |
Sequencing
1. Start with zoom-general (triage and architecture). 2. Add zoom-oauth if any protected resource access is required. 3. Select one primary runtime/API skill (zoom-meeting-sdk, zoom-video-sdk, or zoom-rest-api). 4. Add event/media skills (zoom-webhooks, zoom-websockets, zoom-rtms) based on requirements. 5. Keep the chain minimal; do not add extra skills without explicit need.
Handoff contract
{
"selected_skills": [
"zoom-general",
"zoom-oauth",
"zoom-meeting-sdk",
"zoom-webhooks"
],
"execution_order": [
"zoom-general",
"zoom-oauth",
"zoom-meeting-sdk",
"zoom-webhooks"
],
"assumptions": [
"embedded meeting experience required",
"server-side event endpoint available"
],
"next_actions": [
"confirm OAuth scopes",
"implement auth/token flow",
"implement runtime integration",
"implement event consumer and verification"
]
}Ambiguity handling
If confidence is low, ask one focused question before final routing:
- “Do you need embedded Zoom meetings, or a fully custom video session UI?”
- “Is webhook latency acceptable, or do you require persistent low-latency events?”
Example route
Query: “Build a Linux bot that joins meetings, auto-creates meetings, streams transcript, and tracks lifecycle events.”
Recommended chain:
zoom-generalzoom-oauthzoom-rest-apizoom-meeting-sdkzoom-rtmszoom-webhooks
Why:
zoom-rest-apifor meeting provisioningzoom-meeting-sdkfor runtime join/controlzoom-rtmsfor live transcript/media streamzoom-webhooksfor lifecycle notifications
Routing Implementation (zoom-general)
This reference provides a concrete implementation model for routing a complex developer query from zoom-general to specialized skills.
Runtime Assumptions
- Runtime: Node.js 18+.
- Language: TypeScript 5+.
- Input: free-form developer prompt.
- Output: deterministic handoff contract with primary skill, chained skills, rationale, and follow-up questions (if required).
TypeScript Router Example
export type SkillId =
| 'zoom-general'
| 'zoom-rest-api'
| 'zoom-mcp'
| 'zoom-mcp/whiteboard'
| 'zoom-webhooks'
| 'zoom-websockets'
| 'zoom-meeting-sdk'
| 'zoom-meeting-sdk-web'
| 'zoom-meeting-sdk-web-component-view'
| 'zoom-video-sdk'
| 'zoom-video-sdk-web'
| 'zoom-apps-sdk'
| 'zoom-rtms'
| 'zoom-team-chat'
| 'contact-center'
| 'virtual-agent'
| 'phone'
| 'rivet-sdk'
| 'probe-sdk'
| 'zoom-ui-toolkit'
| 'zoom-cobrowse-sdk'
| 'zoom-oauth';
export interface RouteDecision {
primarySkill: SkillId;
chainedSkills: SkillId[];
confidence: number;
rationale: string[];
needsClarification: string[];
warnings: string[];
}
interface Signals {
meetingEmbed: boolean;
meetingCustomUi: boolean;
customVideo: boolean;
restApi: boolean;
mcp: boolean;
whiteboardMcp: boolean;
webhooks: boolean;
websockets: boolean;
zoomApps: boolean;
oauth: boolean;
rtms: boolean;
teamChat: boolean;
contactCenter: boolean;
virtualAgent: boolean;
phone: boolean;
rivet: boolean;
preflight: boolean;
uiToolkit: boolean;
cobrowse: boolean;
}
const hasAny = (q: string, words: string[]): boolean => words.some((w) => q.includes(w));
export function detectSignals(rawQuery: string): Signals {
const q = rawQuery.toLowerCase();
return {
meetingEmbed: hasAny(q, ['meeting sdk', 'embed meeting', 'join meeting ui', 'client view', 'component view']),
meetingCustomUi: hasAny(q, [
'custom meeting ui',
'custom zoom meeting ui',
'custom meeting video ui',
'custom video ui for meeting',
'zoommtgembedded',
'zoomapproot',
'embeddable meeting ui',
'component view',
]),
customVideo: hasAny(q, ['video sdk', 'custom video', 'attachvideo', 'peer-video-state-change']),
restApi: hasAny(q, ['rest api', 'api create meeting', 'api list meetings', '/v2/', 'list users', 's2s oauth', 'meeting endpoint']),
mcp: hasAny(q, ['zoom mcp', 'mcp server', 'agentic retrieval', 'tools/list', 'tools/call', 'semantic meeting search']),
whiteboardMcp: hasAny(q, ['whiteboard mcp', 'zoom whiteboard mcp', 'list whiteboards', 'get a whiteboard', 'wb/db', 'whiteboard_id']),
webhooks: hasAny(q, ['webhook', 'x-zm-signature', 'event subscription', 'crc']),
websockets: hasAny(q, ['websocket', 'real-time events', 'persistent connection']),
zoomApps: hasAny(q, ['zoom apps sdk', 'in-client app', 'layers api', 'collaborate mode']),
oauth: hasAny(q, ['oauth', 'pkce', 'authorization code', 'account_credentials', 'token refresh']),
rtms: hasAny(q, ['rtms', 'real-time media streams', 'live transcript stream', 'audio stream']),
teamChat: hasAny(q, ['team chat', 'chatbot', 'chat card', 'chat message']),
contactCenter: hasAny(q, ['contact center', 'engagement context', 'contact center smart embed', 'zcc']),
virtualAgent: hasAny(q, ['virtual agent', 'zva', 'knowledge base sync', 'virtual assistant sdk']),
phone: hasAny(q, ['zoom phone', 'phone smart embed', 'phone api', 'click to dial']),
rivet: hasAny(q, ['rivet', 'zoom rivet']),
preflight: hasAny(q, ['probe sdk', 'preflight', 'diagnostics', 'network readiness']),
uiToolkit: hasAny(q, ['ui toolkit', 'prebuilt video ui']),
cobrowse: hasAny(q, ['cobrowse', 'co-browse', 'shared browsing']),
};
}
function pickPrimarySkill(s: Signals): SkillId {
// Hard guardrails: SDK embed/custom-video requests should not fall back to REST.
if (s.meetingCustomUi) return 'zoom-meeting-sdk-web-component-view';
if (s.meetingEmbed && !s.customVideo) return 'zoom-meeting-sdk-web';
if (s.meetingEmbed) return 'zoom-meeting-sdk';
if (s.customVideo && !s.meetingEmbed) return 'zoom-video-sdk-web';
if (s.customVideo) return 'zoom-video-sdk';
if (s.virtualAgent) return 'virtual-agent';
if (s.contactCenter) return 'contact-center';
if (s.zoomApps) return 'zoom-apps-sdk';
if (s.rtms) return 'zoom-rtms';
if (s.teamChat) return 'zoom-team-chat';
if (s.phone) return 'phone';
if (s.cobrowse) return 'zoom-cobrowse-sdk';
if (s.uiToolkit) return 'zoom-ui-toolkit';
if (s.preflight) return 'probe-sdk';
if (s.websockets) return 'zoom-websockets';
if (s.webhooks) return 'zoom-webhooks';
if (s.whiteboardMcp) return 'zoom-mcp/whiteboard';
if (s.mcp) return 'zoom-mcp';
if (s.restApi) return 'zoom-rest-api';
if (s.oauth) return 'zoom-oauth';
return 'zoom-general';
}
function buildChain(primary: SkillId, s: Signals): SkillId[] {
const chain = new Set<SkillId>();
if (primary === 'zoom-meeting-sdk-web-component-view') chain.add('zoom-meeting-sdk-web');
// Auth chaining.
if (s.oauth || s.restApi || s.mcp || s.webhooks || s.websockets || s.phone || s.teamChat || s.virtualAgent) {
chain.add('zoom-oauth');
}
// Optional server framework.
if (s.rivet) chain.add('rivet-sdk');
// Cross-surface chaining.
if (primary === 'contact-center' && s.virtualAgent) chain.add('virtual-agent');
if (primary === 'virtual-agent' && s.contactCenter) chain.add('contact-center');
// Event channels often pair with REST resource management.
if (s.webhooks || s.websockets) chain.add('zoom-rest-api');
if (s.mcp && s.restApi) {
chain.add('zoom-rest-api');
chain.add('zoom-mcp');
}
// Avoid redundant primary in chain.
chain.delete(primary);
return [...chain];
}
function validateDecision(primary: SkillId, s: Signals): string[] {
const warnings: string[] = [];
if (s.meetingEmbed && !['zoom-meeting-sdk', 'zoom-meeting-sdk-web', 'zoom-meeting-sdk-web-component-view'].includes(primary)) {
warnings.push('meeting embed intent detected but primary skill is not zoom-meeting-sdk');
}
if (s.meetingCustomUi && primary !== 'zoom-meeting-sdk-web-component-view') {
warnings.push('custom meeting UI intent detected but primary skill is not zoom-meeting-sdk-web-component-view');
}
if (s.customVideo && !['zoom-video-sdk', 'zoom-video-sdk-web'].includes(primary)) {
warnings.push('custom video intent detected but primary skill is not zoom-video-sdk');
}
if (s.meetingCustomUi && s.customVideo) {
warnings.push('meeting UI intent and custom video intent both detected; prefer Meeting SDK Component View unless the user explicitly wants a non-meeting session');
}
if (s.restApi && (s.meetingEmbed || s.customVideo)) {
warnings.push('mixed SDK + REST intent; keep SDK as primary and use REST only for resource workflows');
}
return warnings;
}
function confidenceFromSignals(s: Signals): number {
const hits = Object.values(s).filter(Boolean).length;
if (hits >= 4) return 0.9;
if (hits >= 2) return 0.78;
if (hits === 1) return 0.65;
return 0.5;
}
export function routeComplexQuery(query: string): RouteDecision {
const signals = detectSignals(query);
const primarySkill = pickPrimarySkill(signals);
const chainedSkills = buildChain(primarySkill, signals);
const warnings = validateDecision(primarySkill, signals);
const needsClarification: string[] = [];
if (signals.mcp && signals.restApi) {
needsClarification.push('Do you want deterministic REST API automation, AI-agent MCP tooling, or a hybrid of both?');
}
if (primarySkill === 'zoom-general') {
needsClarification.push('Do you need SDK embed behavior, API resource automation, or event ingestion?');
}
const rationale = [
`primary=${primarySkill}`,
`signals=${JSON.stringify(signals)}`,
`chained=${chainedSkills.join(',') || 'none'}`,
];
return {
primarySkill,
chainedSkills,
confidence: confidenceFromSignals(signals),
rationale,
needsClarification,
warnings,
};
}Handoff Contract (Example Output)
{
"primarySkill": "zoom-meeting-sdk",
"chainedSkills": ["zoom-oauth", "zoom-rest-api", "zoom-webhooks"],
"confidence": 0.9,
"rationale": [
"primary=zoom-meeting-sdk",
"signals={\"meetingEmbed\":true,\"restApi\":true,\"webhooks\":true,...}",
"chained=zoom-oauth,zoom-rest-api,zoom-webhooks"
],
"needsClarification": [],
"warnings": [
"mixed SDK + REST intent; keep SDK as primary and use REST only for resource workflows"
]
}Error Handling Expectations
- Unknown/low-signal prompts route to
zoom-generalwith one clarifying question. - Conflicting signals do not fail hard; produce warnings and preserve guardrails.
- Routing should be deterministic for the same normalized prompt.
OAuth Scopes
OAuth scopes define what your app can access.
Overview
Scopes are permissions requested during OAuth authorization. Request only the scopes you need.
IMPORTANT: Scope Types
Different OAuth types have different scopes available:
| OAuth Type | Scope Suffix | Access Level | Example |
|---|---|---|---|
| User OAuth | (none) | Current user's data only | meeting:read |
| Admin OAuth | :admin | All users in account | meeting:read:admin |
| Server-to-Server (S2S) | :admin | All users in account (no user consent) | meeting:read:admin |
Key Differences
- User scopes (
meeting:read): Access only the authenticated user's data - Admin scopes (
meeting:read:admin): Access data for ALL users in the account - S2S OAuth: Uses admin-level scopes but doesn't require user login - intended for backend integrations
Choosing the Right Scope Type
| Use Case | OAuth Type | Scope Example |
|---|---|---|
| User manages their own meetings | User OAuth | meeting:write |
| Admin dashboard for all users | Admin OAuth | meeting:read:admin |
| Backend automation (no user login) | Server-to-Server | meeting:write:admin |
| Bot that creates meetings for users | Server-to-Server | meeting:write:admin |
Common Scopes
Meetings
| User Scope | Admin Scope | Description |
|---|---|---|
meeting:read | meeting:read:admin | View meeting details |
meeting:write | meeting:write:admin | Create, update, delete meetings |
meeting:master | meeting:master:admin | Full meeting access |
Users
| User Scope | Admin Scope | Description |
|---|---|---|
user:read | user:read:admin | View user profile |
user:write | user:write:admin | Update user settings |
user:master | user:master:admin | Full user access |
Recordings
| User Scope | Admin Scope | Description |
|---|---|---|
recording:read | recording:read:admin | View/download recordings |
recording:write | recording:write:admin | Delete recordings |
recording:master | recording:master:admin | Full recording access |
Webinars
| User Scope | Admin Scope | Description |
|---|---|---|
webinar:read | webinar:read:admin | View webinar details |
webinar:write | webinar:write:admin | Create, update webinars |
webinar:master | webinar:master:admin | Full webinar access |
Reports
| User Scope | Admin Scope | Description |
|---|---|---|
report:read | report:read:admin | View reports and analytics |
report:master | report:master:admin | Full report access |
Scope Patterns
| Pattern | Meaning |
|---|---|
resource:read | Read-only access (current user) |
resource:write | Read and write access (current user) |
resource:master | Full access including delete (current user) |
resource:read:admin | Read-only access (all account users) |
resource:write:admin | Read and write access (all account users) |
resource:master:admin | Full access including delete (all account users) |
Best Practices
1. Request minimum scopes - Only what you need 2. Explain to users - Why you need each scope 3. Handle denied scopes - Graceful fallback
Resources
- Scopes reference: https://developers.zoom.us/docs/integrations/oauth-scopes/
SDK Logs & Troubleshooting
Collecting SDK logs for debugging and support.
Official Log Retrieval Guides
IMPORTANT: Always refer to the official Zoom log retrieval guides for the most up-to-date instructions:
- Video SDK Log Retrieval: https://developers.zoom.us/blog/vsdk-log-retrieval-instructions/
- Meeting SDK Log Retrieval: https://developers.zoom.us/blog/msdk-log-retrieval-instructions/
If these URLs are unavailable, search for "zoom sdk log retrieval" to find the current documentation.
Overview
SDK logs help diagnose issues during development and for Zoom support escalations.
Enabling Logs
Web SDK
// Enable verbose logging
ZoomMtg.setLogLevel('verbose');
// Or for Video SDK
client.init('en-US', 'CDN', { debug: true });Web Tracking ID: For Web SDK troubleshooting, get the Web Tracking ID which helps Zoom support trace your session.
Meeting SDK Web: 1. Open browser DevTools → Network tab 2. Look for a request starting with info?meetingNumber... 3. Click on the request and check the Response Headers 4. Find the x-zm-trackingid header value 5. Copy this ID for support tickets
Video SDK Web: 1. Open browser DevTools → Network tab 2. Look for a request starting with lsdk?topic... 3. Click on the request and check the Response Headers 4. Find the x-zm-trackingid header value 5. Copy this ID for support tickets
Example header:
x-zm-trackingid: v=2.0;clid=us04;rid=WEB_abc123xyz...The Web Tracking ID is essential for Zoom support to investigate Web SDK issues.
To get help with logs and tracking IDs:
- Open a support ticket: https://devsupport.zoom.us/
- Post on Developer Forum: https://devforum.zoom.us/
Include the tracking ID and relevant logs when requesting assistance.
iOS SDK
// Set log file path
let initParams = MobileRTCSDKInitParams()
initParams.enableLog = true
initParams.logFilePrefix = "zoom_sdk"Android SDK
val initParams = ZoomSDKInitParams().apply {
enableLog = true
logSize = 5 // MB
}Desktop SDKs (Windows/macOS/Linux)
initParam.enableLogByDefault = true;
initParam.logFilePrefix = L"zoom_sdk";Log Locations
| Platform | Default Location |
|---|---|
| iOS | App's Documents directory |
| Android | App's files directory |
| Windows | %APPDATA%\ZoomSDK\ |
| macOS | ~/Library/Logs/ZoomSDK/ |
| Linux | Working directory |
Common Issues and Solutions
| Issue | Possible Cause | Solution |
|---|---|---|
| Join failed | Invalid signature | Check JWT generation (exp should be ~10s after iat, but iat can be up to 2 hours in past) |
| Join failed | Meeting not found | Verify meeting number and that meeting hasn't ended |
| No audio | Permission denied | Request microphone permission before joining |
| No video | Permission denied | Request camera permission before joining |
| Video scales down | Container too small | Ensure container is at least 1280x720 for 720p |
| SharedArrayBuffer error | Missing headers | Add COOP/COEP headers to server |
| Error code 0 | Actually success | Check SDK docs - 0 often means success, not error |
| SDK crash | ProGuard enabled | Disable ProGuard/R8 for Zoom SDK classes |
| DLL not found | Missing files | Copy ALL DLLs from SDK bin folder |
Debugging Join Failures
// Web SDK - enable verbose logging
ZoomMtg.setLogLevel('verbose');
// Check signature
console.log('Signature:', signature);
console.log('Meeting:', meetingNumber);
// Verify callback
client.join({
// ...params
success: (res) => console.log('Join success:', res),
error: (err) => console.error('Join error:', err)
});Debugging Audio/Video Issues
// Check device availability
const devices = await navigator.mediaDevices.enumerateDevices();
console.log('Audio inputs:', devices.filter(d => d.kind === 'audioinput'));
console.log('Video inputs:', devices.filter(d => d.kind === 'videoinput'));
// Check permissions
const micPermission = await navigator.permissions.query({ name: 'microphone' });
const camPermission = await navigator.permissions.query({ name: 'camera' });
console.log('Mic:', micPermission.state);
console.log('Cam:', camPermission.state);Native SDK Crash Debugging
iOS:
// Enable crash reporting
MobileRTC.shared().setEnableCrashReport(true)
// Get logs
let logPath = MobileRTC.shared().getLogPath()
print("Logs at: \(logPath)")Android:
// Check logcat for crashes
adb logcat -s ZoomSDK:V
// Disable ProGuard for Zoom
// In proguard-rules.pro:
-keep class us.zoom.** { *; }
-keep interface us.zoom.** { *; }Error Code Reference
| Code | Meaning | Platform |
|---|---|---|
| 0 | Success (NOT error!) | All |
| 1 | Generic error | All |
| 2 | Invalid argument | All |
| 3 | Invalid token | All |
| 4 | Timeout | All |
| 8 | SDK not authorized | Windows |
| 100000400 | Meeting join failed | Windows |
| MobileRTCMeetError_Success | Success | iOS |
| MEETING_STATUS_INMEETING | In meeting | Android |
Getting Support
When contacting Zoom support:
1. Include SDK version 2. Attach log files 3. Describe steps to reproduce 4. Include error codes (check if 0 means success!)
Resources
- Video SDK Log Retrieval Guide: https://developers.zoom.us/blog/vsdk-log-retrieval-instructions/
- Meeting SDK Log Retrieval Guide: https://developers.zoom.us/blog/msdk-log-retrieval-instructions/
- Developer support: https://devsupport.zoom.us/
- Developer forum: https://devforum.zoom.us/
Note for LLMs/Agents: If the log retrieval URLs return 404 or have moved, perform a web search for "zoom sdk log retrieval instructions" to find the current documentation.
SDK Upgrade Guide
Guide for upgrading Meeting SDK and Video SDK versions.
For customer upgrades from older versions to latest, use:
- sdk-upgrade-workflow.md - changelog + RSS, version-by-version migration workflow.
IMPORTANT: Check the Changelog First
Before any upgrade, always check the official Zoom changelog:
Primary URL: https://developers.zoom.us/changelog/
If the above URL is unavailable or has moved, search for "zoom changelog" or "zoom developer changelog" to find the current location.
The changelog contains:
- Latest SDK versions and release dates
- Breaking changes and deprecations
- New features and improvements
- Bug fixes and security patches
Overview
Zoom releases SDK updates regularly. This guide covers version policy and upgrade procedures.
Version Policy
- Major versions - May contain breaking changes
- Minor versions - New features, backward compatible
- Patch versions - Bug fixes
Before Upgrading
1. Read changelog for target version 2. Note breaking changes and deprecations 3. Test in development environment 4. Plan migration for deprecated APIs
Upgrade Steps
Web SDK (npm)
# Check current version
npm list @zoom/meetingsdk
# Update to latest
npm update @zoom/meetingsdk
# Or specific version
npm install @zoom/meetingsdk@2.18.0Native SDKs
1. Download new SDK from Marketplace (sign-in required) 2. Replace SDK files in your project 3. Update linker/framework settings if needed 4. Rebuild project
Common Migration Tasks
API Signature Changes
When methods change signatures between versions:
// Old (v2.x)
client.join({
sdkKey: key,
sdkSecret: secret, // REMOVED in v3.x
meetingNumber: number
});
// New (v3.x) - signature generated server-side
client.join({
sdkKey: key,
signature: serverGeneratedSignature, // NEW
meetingNumber: number
});Action: Update to server-side signature generation for security.
Deprecated Method Replacements
| Old Method | New Method | Version |
|---|---|---|
ZoomMtg.init() | client.init() | Web SDK 3.x |
startVideo() | startVideo() + renderVideo() | Video SDK 1.8+ |
getMeetingUUID() | Use webhook payload | Meeting SDK 2.x |
New Initialization Requirements
Meeting SDK Web 3.x:
// Now requires explicit preload
import ZoomMtgEmbedded from '@zoom/meetingsdk/embedded';
const client = ZoomMtgEmbedded.createClient();
// Must init before join
await client.init({
zoomAppRoot: document.getElementById('root'),
language: 'en-US'
});Video SDK 1.8+:
// Video rendering is now two-step
await stream.startVideo();
await stream.renderVideo(
document.querySelector('#video-canvas'),
myUserId,
1280, 720, 0, 0, 3 // width, height, x, y, quality
);Breaking Changes Checklist
When upgrading major versions, check:
- [ ] Initialization flow changed?
- [ ] Authentication method changed?
- [ ] Event names/signatures changed?
- [ ] Required permissions changed?
- [ ] Minimum platform version changed?
- [ ] New required headers (COOP/COEP)?
Testing Upgrade
# Create upgrade branch
git checkout -b sdk-upgrade-v3
# Update package
npm install @zoom/meetingsdk@latest
# Run tests
npm test
# Test manually
# - Join meeting
# - Audio/video functionality
# - Screen sharing
# - Recording (if used)
# - Custom UI featuresVersion Support Policy
- Latest version: Full support
- Previous major: Security fixes only
- Older versions: No support, upgrade recommended
Resources
- Main Changelog: https://developers.zoom.us/changelog/ (check here first!)
- Meeting SDK changelog: https://developers.zoom.us/changelog/meeting-sdk/
- Video SDK changelog: https://developers.zoom.us/changelog/video-sdk/
- Migration guides: https://developers.zoom.us/docs/meeting-sdk/web/migrate/
Note for LLMs/Agents: If the changelog URLs return 404 or have moved, perform a web search for "zoom developer changelog" or "zoom sdk changelog" to find the current location. Zoom occasionally restructures their documentation.
SDK Upgrade Workflow (Changelog + RSS)
Reusable process for upgrading Zoom SDK integrations from an older customer version to latest with low regression risk.
Use This When
- Customer is multiple versions behind.
- Breaking changes may exist between current and latest.
- You need a defensible, version-by-version upgrade plan.
Inputs Required
1. Product and platform
- Example:
Meeting SDK Android,Video SDK iOS,Contact Center Web.
2. Current version in production
- Example:
6.3.1,2.1.0.
3. Target version
- Usually latest stable from changelog.
4. Critical features in use
- Example: custom UI, raw data, recording, chat, live transcription, token flow.
Canonical Source
- Changelog entry point: https://developers.zoom.us/changelog/
Workflow
1) Scope the upgrade lane
- Confirm exact product + platform lane before collecting releases.
- Do not mix lanes (for example, Meeting SDK Web and Meeting SDK iOS must be treated separately).
2) Locate the platform-specific RSS feed
From https://developers.zoom.us/changelog/:
- Filter by product/platform.
- Find the RSS link for that filtered lane.
- Use only that feed for release collection.
If feed discovery is unclear:
- Open the filtered changelog page and locate the RSS icon/link.
- Confirm feed entries match the same product/platform lane.
3) Build the release ledger
Collect all releases from:
current_version(exclusive) up totarget_version(inclusive), then latest if target islatest.
For each release entry capture:
- Version
- Release date
- Release URL
- Breaking/deprecated notes
- Required migration actions
Sort upgrade steps in ascending version order.
4) Plan upgrade hops
Default strategy:
- Patch/minor jumps can often be grouped.
- Major changes should be isolated into dedicated hops.
Recommended hop pattern: 1. current -> next safe checkpoint 2. checkpoint -> next major boundary 3. Repeat until latest
5) Extract required actions per hop
For each hop, classify actions under:
- Auth/token contract changes
- API renames/signature changes
- Initialization/lifecycle changes
- Event payload/callback changes
- Build/dependency/runtime requirements
- Feature removals/deprecations
6) Apply compatibility guards
- Wrap renamed/deprecated calls behind adapters.
- Keep temporary compatibility mappings for payload changes.
- Add feature flags for behavior toggles when needed.
7) Validate each hop before continuing
Minimum validation set:
- SDK init/auth
- Join/start/session entry flow
- Core media flows (audio/video/share) if applicable
- Critical product-specific features used by customer
- Cleanup/leave/disconnect behavior
Do not skip to next hop if the current hop is unstable.
8) Produce final upgrade package
Deliver:
- Step-by-step upgrade matrix
- Per-hop code/config change list
- Deprecated-to-replacement map
- Risks and rollback notes
- Final target-state checklist
Output Template
## Upgrade Plan: <product/platform>
- Current: <x.y.z>
- Target: <a.b.c or latest>
- Source feed: <rss_url>
### Hop 1: <x.y.z -> x.y+1.z>
- Release notes:
- <url>
- Breaking/deprecations:
- <item>
- Required changes:
- <item>
- Validation:
- <item>
### Hop 2: <...>
...
## Deprecated -> Replacement Map
- <old> -> <new>
## Risks
- <risk>
## Rollback
- <rollback step>Operating Rules
- Never assume only latest release notes are sufficient.
- Always process intermediate releases between customer version and target.
- Prefer smallest-risk path over fastest path for production upgrades.
General Skill 5-Minute Preflight Runbook
Use this before deep debugging.
Skill Doc Standard Note
- Skill entrypoint is
SKILL.md. - This runbook is an operational convention (recommended), not a required skill file.
- SDK/API names can drift by version; validate current names against docs/raw-docs before release.
1) Confirm Integration Surface
- Use
generalas the routing hub for cross-product intent selection. - Confirm each use-case links to the correct product skill chain.
- Use this runbook before troubleshooting multi-product integrations.
2) Confirm Required Credentials
- Validate OAuth model selection (User OAuth vs Server-to-Server OAuth) before implementation.
- Ensure required scopes are documented in each use-case.
- Keep credential storage server-side; only expose short-lived tokens to clients.
3) Confirm Lifecycle Order
1. Pick product path (REST, Meeting SDK, Video SDK, Apps SDK, Phone, Contact Center, etc.). 2. Map auth flow and required scopes. 3. Define event model (webhooks or websockets) and correlation IDs. 4. Validate deployment model and operational monitoring requirements.
4) Confirm Event/State Handling
- Keep use-case assumptions explicit when combining multiple products.
- Store cross-system identifiers (meeting/session/call/engagement IDs) for traceability.
- Document fallback behavior when API names/fields drift between versions.
5) Confirm Cleanup + Upgrade Posture
- Remove stale route links whenever skills are renamed or moved.
- Keep
.envkey references centralized in environment variable reference docs. - Refresh compatibility notes after each major SDK/API update cycle.
6) Quick Probes
- Routing matrix still points to existing
SKILL.mdfiles. - Use-cases include at least one concrete implementation chain.
- OAuth/scopes guidance matches current Marketplace app model.
7) Fast Decision Tree
- Unsure between Meeting SDK and Video SDK -> route by UX model (Zoom meeting UI vs fully custom session).
- Need lowest-latency events -> use websockets; otherwise webhooks are acceptable.
- Scope/auth failures in execution -> pause and re-authorize with correct app type and scopes.
8) Source Checkpoints
Official docs
- https://developers.zoom.us/
- https://marketplace.zoom.us/
- https://devforum.zoom.us/
Raw docs in repo
raw-docs/developers.zoom.us/docs/raw-docs/marketplacefront.zoom.us/sdk/
APIs vs MCP Routing
Decide whether to route a request to Zoom APIs, Zoom MCP, or both.
Overview
Zoom APIs and Zoom MCP are complementary:
- Zoom APIs are best for deterministic system integrations.
- Zoom MCP is best for AI-driven tool-based workflows.
- Use both for enterprise AI systems that need a stable automation core and an adaptive AI layer.
- Zoom-hosted MCP follows a product-scoped server model; access is OAuth-scoped and governed.
Decision Matrix
| Primary requirement | Route | Notes |
|---|---|---|
| Deterministic automation, configuration, reporting, scheduled jobs, strict retries/error handling | zoom-rest-api | Direct control over requests, retries, and idempotency |
| AI interaction, dynamic tool discovery, AI Companion workflows, external AI interoperability | zoom-mcp | Agent chooses tools contextually through MCP |
| High-volume production automation plus AI assistant workflows | zoom-rest-api + zoom-mcp | Keep core actions in APIs; expose curated tool surfaces via MCP |
Typical Routing Examples
| User request | Route |
|---|---|
| "Create meetings nightly and sync metrics to BI" | zoom-rest-api |
| "Let my assistant search meeting content and fetch transcripts" | zoom-mcp |
| "Automate meeting lifecycle, then let agents answer questions from summaries" | zoom-rest-api + zoom-mcp |
Chaining Patterns
Pattern A: API-only deterministic backend
1. zoom-oauth for app auth/token lifecycle. 2. zoom-rest-api for create/read/update/reporting endpoints. 3. zoom-webhooks for async event processing if needed.
Pattern B: MCP-first AI tool workflows
1. zoom-oauth for user OAuth token required by MCP server. 2. zoom-mcp for semantic meeting search, summaries, recordings/transcripts, and tool invocation.
Pattern C: Hybrid enterprise AI architecture
1. zoom-rest-api handles provisioning, policy/configuration, and scheduled ingestion jobs. 2. zoom-webhooks or zoom-websockets handles event ingestion. 3. zoom-mcp exposes curated higher-level tools for AI Companion or external agents.
MCP Fit Checklist (FAQ-Aligned)
Use zoom-mcp when you are:
- Building custom tools for AI models.
- Creating data integration services for AI assistants.
- Developing specialized assistants that need tool discovery.
- Extending AI capabilities with external services via MCP.
- Building enterprise AI solutions that need interoperable agent tooling.
MCP Client and Transport Constraints
- Zoom remote MCP server is consumed over Streamable HTTP/SSE.
- Typical supported MCP clients include Claude and VS Code MCP-capable tooling.
- A local stdio mode may be available depending on client setup, but remote Zoom MCP routing assumes HTTP/SSE transport.
- Endpoint model is shared by instance/cluster; do not assume per-customer dedicated endpoint generation.
- MCP server surfaces can be product-scoped (for example Meetings, Team Chat, Whiteboard). Route by product when those surfaces are available.
Routing Guardrails
- Do not route deterministic backend automation to MCP only.
- Do not route AI-agent tool discovery tasks to REST only.
- Prefer hybrid routing when both deterministic backend operations and AI-driven interactions are required.
Related Skills
- zoom-rest-api
- zoom-mcp
- zoom-oauth
- zoom-webhooks
- zoom-websockets
Source
- https://developers.zoom.us/docs/mcp/library/resources/apis-vs-mcp/
Flutter Video Sessions
Use this flow when you are building custom real-time video sessions in a Flutter mobile app.
When to Use
- You need full control over UI/UX (not Zoom Meeting UI).
- You are building iOS/Android mobile session experiences in Flutter.
- You need helper-driven features such as chat, share, recording, or transcription.
Skill Chain
1. video-sdk/flutter 2. zoom-oauth
Typical Flow
1. Backend signs short-lived Video SDK JWT. 2. Flutter app initializes SDK and binds event listeners. 3. App joins session and activates media/helpers. 4. App leaves and cleans up explicitly.
References
- Flutter Video SDK Skill
- Lifecycle Workflow
- Session Join Pattern
Related skills
Forks & variants (1)
Zoom General has 1 known copy in the catalog totaling 18 installs. They canonicalize to this original listing.
- zoom - 18 installs
How it compares
Use zoom-general for Marketplace app registration decisions; follow with choose-zoom-approach for API surface selection and SDK-specific build skills.
FAQ
What does zoom-general do?
Cross-product Zoom reference skill. Use after the workflow is clear when you need shared platform guidance, app-model comparisons, authentication context, scopes, marketplace.
When should I use zoom-general?
User asks about zoom general or related SKILL.md workflows.
Is zoom-general safe to install?
Review the Security Audits panel on this page before installing in production.