
Build Zoom Rest Api App
- 1.4k installs
- 23.1k repo stars
- Updated July 28, 2026
- anthropics/knowledge-work-plugins
build-zoom-rest-api-app is an agent skill for reference skill for zoom rest api. use after choosing an api-based workflow when you need endpoint selection, resource-management patterns, oauth requirements, rate-limit.
About
The build-zoom-rest-api-app skill is designed for reference skill for Zoom REST API. Use after choosing an API-based workflow when you need endpoint selection, resource-management patterns, OAuth requirements, rate-limit. /build-zoom-rest-api-app Background reference for deterministic server-side Zoom automation and resource management. Prefer plan-zoom-product, plan-zoom-integration, or debug-zoom first, then route here for endpoint-level detail. Invoke when the user asks about build zoom rest api app or related SKILL.md workflows.
- Meetings - Meeting CRUD, types, settings.
- Users - User provisioning and management.
- Recordings - Cloud recording access and download.
- AI Services - Scribe endpoint inventory and current AI Services path surface.
- GraphQL Queries - Alternative query API (beta).
Build Zoom Rest Api App by the numbers
- 1,443 all-time installs (skills.sh)
- +83 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #243 of 1,896 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
build-zoom-rest-api-app capabilities & compatibility
- Capabilities
- meetings meeting crud, types, settings · users user provisioning and management · recordings cloud recording access and download · ai services scribe endpoint inventory and curr
- Use cases
- frontend
What build-zoom-rest-api-app says it does
Reference skill for Zoom REST API. Use after choosing an API-based workflow when you need endpoint selection, resource-management patterns, OAuth requirements, rate-limit awareness
Reference skill for Zoom REST API. Use after choosing an API-based workflow when you need endpoint selection, resource-management patterns, OAuth requirements,
npx skills add https://github.com/anthropics/knowledge-work-plugins --skill build-zoom-rest-api-appAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 23.1k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | anthropics/knowledge-work-plugins ↗ |
How do I reference skill for zoom rest api. use after choosing an api-based workflow when you need endpoint selection, resource-management patterns, oauth requirements, rate-limit?
Reference skill for Zoom REST API. Use after choosing an API-based workflow when you need endpoint selection, resource-management patterns, OAuth requirements, rate-limit.
Who is it for?
Developers using build zoom rest api app workflows documented in SKILL.md.
Skip if: Skip when the task falls outside build-zoom-rest-api-app scope or needs a different stack.
When should I use this skill?
User asks about build zoom rest api app or related SKILL.md workflows.
What you get
Completed build-zoom-rest-api-app workflow with documented commands, files, and expected deliverables.
- Regional Zoom API client configuration
- Authenticated request handler code
By the numbers
- Documents Zoom REST API version /v2 and GraphQL endpoint /v3/graphql
- Includes regional routing table for EU and global api_url hosts
Files
/build-zoom-rest-api-app
Background reference for deterministic server-side Zoom automation and resource management. Prefer plan-zoom-product, plan-zoom-integration, or debug-zoom first, then route here for endpoint-level detail.
Zoom REST API
Expert guidance for building server-side integrations with the Zoom REST API. This API provides 600+ endpoints for managing meetings, users, webinars, recordings, reports, and all Zoom platform resources programmatically.
Official Documentation: https://developers.zoom.us/api-hub/ API Hub Reference: https://developers.zoom.us/api-hub/meetings/ OpenAPI Inventories: https://developers.zoom.us/api-hub/<domain>/methods/endpoints.json
Quick Links
New to Zoom REST API? Follow this path:
1. [API Architecture](concepts/api-architecture.md) - Base URLs, regional URLs, me keyword, ID vs UUID, time formats 2. [Authentication Flows](concepts/authentication-flows.md) - OAuth setup (S2S, User, PKCE, Device Code) 3. [Meeting URLs vs Meeting SDK](concepts/meeting-urls-and-sdk-joining.md) - Stop mixing join_url with Meeting SDK 3. [Meeting Lifecycle](examples/meeting-lifecycle.md) - Create → Update → Start → End → Delete with webhooks 4. [Rate Limiting Strategy](concepts/rate-limiting-strategy.md) - Plan tiers, per-user limits, retry patterns
Reference:
- [Meetings](references/meetings.md) - Meeting CRUD, types, settings
- [Users](references/users.md) - User provisioning and management
- [Recordings](references/recordings.md) - Cloud recording access and download
- [AI Services](references/ai-services.md) - Scribe endpoint inventory and current AI Services path surface
- [GraphQL Queries](examples/graphql-queries.md) - Alternative query API (beta)
- Integrated Index - see the section below in this file
Most domain files under references/ are aligned to the official API Hub endpoints.json inventories. Treat those files as the local source of truth for method/path discovery.
Having issues?
- Start with preflight checks → 5-Minute Runbook
- 401 Unauthorized → Authentication Flows (check token expiry, scopes)
- 429 Too Many Requests → Rate Limiting Strategy
- Error codes → Common Errors
- Pagination confusion → Common Issues
- Webhooks not arriving → Webhook Server
- Forum-derived FAQs → Forum Top Questions
- Token/scope failures → Token + Scope Playbook
Building event-driven integrations?
- Webhook Server - Express.js server with CRC validation
- Recording Pipeline - Auto-download via webhook events
Quick Start
Get an Access Token (Server-to-Server OAuth)
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=account_credentials&account_id=ACCOUNT_ID"Response:
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "meeting:read meeting:write user:read"
}Create a Meeting
curl -X POST "https://api.zoom.us/v2/users/HOST_USER_ID/meetings" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"topic": "Team Standup",
"type": 2,
"start_time": "2025-03-15T10:00:00Z",
"duration": 30,
"settings": {
"join_before_host": false,
"waiting_room": true
}
}'For S2S OAuth, use an explicit host user ID or email in the path. Do not use me.
List Users with Pagination
curl "https://api.zoom.us/v2/users?page_size=300&status=active" \
-H "Authorization: Bearer ACCESS_TOKEN"Base URL
https://api.zoom.us/v2Regional Base URLs
The api_url field in OAuth token responses indicates the user's region. Use regional URLs for data residency compliance:
| Region | URL |
|---|---|
| Global (default) | https://api.zoom.us/v2 |
| Australia | https://api-au.zoom.us/v2 |
| Canada | https://api-ca.zoom.us/v2 |
| European Union | https://api-eu.zoom.us/v2 |
| India | https://api-in.zoom.us/v2 |
| Saudi Arabia | https://api-sa.zoom.us/v2 |
| Singapore | https://api-sg.zoom.us/v2 |
| United Kingdom | https://api-uk.zoom.us/v2 |
| United States | https://api-us.zoom.us/v2 |
Note: You can always use the global URL https://api.zoom.us regardless of the api_url value.
Key Features
| Feature | Description |
|---|---|
| Meeting Management | Create, read, update, delete meetings with full scheduling control |
| User Provisioning | Automated user lifecycle (create, update, deactivate, delete) |
| Webinar Operations | Webinar CRUD, registrant management, panelist control |
| Cloud Recordings | List, download, delete recordings with file-type filtering |
| Reports & Analytics | Usage reports, participant data, daily statistics |
| Team Chat | Channel management, messaging, chatbot integration |
| Zoom Phone | Call management, voicemail, call routing |
| Zoom Rooms | Room management, device control, scheduling |
| Webhooks | Real-time event notifications for 100+ event types |
| WebSockets | Persistent event streaming without public endpoints |
| GraphQL (Beta) | Single-endpoint flexible queries at v3/graphql |
| AI Companion | Meeting summaries, transcripts, AI-generated content |
| AI Services / Scribe | File and archive transcription via Build-platform JWT-authenticated endpoints |
Prerequisites
- Zoom account (Free tier has API access with lower rate limits)
- App registered on Zoom App Marketplace
- OAuth credentials (Server-to-Server OAuth or User OAuth)
- Appropriate scopes for target endpoints
Need help with authentication? See the [zoom-oauth](../oauth/SKILL.md) skill for complete OAuth flow implementation.
Critical Gotchas and Best Practices
⚠️ JWT App Type is Deprecated
The JWT app type is deprecated. Migrate to Server-to-Server OAuth. This does NOT affect JWT token signatures used in Video SDK — only the Marketplace "JWT" app type for REST API access.
// OLD (JWT app type - DEPRECATED)
const token = jwt.sign({ iss: apiKey, exp: expiry }, apiSecret);
// NEW (Server-to-Server OAuth)
const token = await getServerToServerToken(accountId, clientId, clientSecret);⚠️ The me Keyword Rules
- User-level OAuth apps: MUST use
meinstead ofuserId(otherwise: invalid token error) - Server-to-Server OAuth apps: MUST NOT use
me— provide the actualuserIdor email - Account-level OAuth apps: Can use either
meoruserId
⚠️ Meeting ID vs UUID — Double Encoding
UUIDs that begin with / or contain // must be double URL-encoded:
// UUID: /abc==
// Single encode: %2Fabc%3D%3D
// Double encode: %252Fabc%253D%253D ← USE THIS
const uuid = '/abc==';
const encoded = encodeURIComponent(encodeURIComponent(uuid));
const url = `https://api.zoom.us/v2/meetings/${encoded}`;⚠️ Time Formats
yyyy-MM-ddTHH:mm:ssZ— UTC time (note theZsuffix)yyyy-MM-ddTHH:mm:ss— Local time (noZ, usestimezonefield)- Some report APIs only accept UTC. Check the API reference for each endpoint.
⚠️ Rate Limits Are Per-Account, Not Per-App
All apps on the same Zoom account share rate limits. One heavy app can impact others. Monitor X-RateLimit-Remaining headers proactively.
⚠️ Per-User Daily Limits
Meeting/Webinar create/update operations are limited to 100 per day per user (resets at 00:00 UTC). Distribute operations across different host users when doing bulk operations.
⚠️ Download URLs Require Auth and Follow Redirects
Recording download_url values require Bearer token authentication and may redirect. Always follow redirects:
curl -L -H "Authorization: Bearer ACCESS_TOKEN" "https://zoom.us/rec/download/..."Use Webhooks Instead of Polling
// DON'T: Poll every minute (wastes API quota)
setInterval(() => getMeetings(), 60000);
// DO: Receive webhook events in real-time
app.post('/webhook', (req, res) => {
if (req.body.event === 'meeting.started') {
handleMeetingStarted(req.body.payload);
}
res.status(200).send();
});Webhook setup details: See the [zoom-webhooks](../webhooks/SKILL.md) skill for comprehensive webhook implementation.
Complete Documentation Library
This skill includes comprehensive guides organized by category:
Core Concepts
- [API Architecture](concepts/api-architecture.md) - REST design, base URLs, regional routing,
mekeyword, ID vs UUID, time formats - [Authentication Flows](concepts/authentication-flows.md) - All OAuth flows (S2S, User, PKCE, Device Code)
- [Rate Limiting Strategy](concepts/rate-limiting-strategy.md) - Limits by plan, retry patterns, request queuing
Complete Examples
- [Meeting Lifecycle](examples/meeting-lifecycle.md) - Full Create → Update → Start → End → Delete flow with webhook events
- [User Management](examples/user-management.md) - CRUD users, list with pagination, bulk operations
- [Recording Pipeline](examples/recording-pipeline.md) - Download recordings via webhooks + API
- [Webhook Server](examples/webhook-server.md) - Express.js server with CRC validation and signature verification
- [GraphQL Queries](examples/graphql-queries.md) - GraphQL queries, mutations, cursor pagination
Troubleshooting
- [Common Errors](troubleshooting/common-errors.md) - HTTP status codes, Zoom error codes, error response formats
- [Common Issues](troubleshooting/common-issues.md) - Rate limits, token refresh, pagination pitfalls, gotchas
References (39 files covering all Zoom API domains)
Core APIs
- [references/meetings.md](references/meetings.md) - Meeting CRUD, types, settings
- [references/users.md](references/users.md) - User provisioning, types, scopes
- [references/webinars.md](references/webinars.md) - Webinar management, registrants
- [references/recordings.md](references/recordings.md) - Cloud recording access
- [references/reports.md](references/reports.md) - Usage reports, analytics
- [references/accounts.md](references/accounts.md) - Account management
Communication
- [references/team-chat.md](references/team-chat.md) - Team Chat messaging
- [references/chatbot.md](references/chatbot.md) - Interactive chatbots
- [references/phone.md](references/phone.md) - Zoom Phone
- [references/mail.md](references/mail.md) - Zoom Mail
- [references/calendar.md](references/calendar.md) - Zoom Calendar
Infrastructure
- [references/rooms.md](references/rooms.md) - Zoom Rooms
- [references/scim2.md](references/scim2.md) - SCIM 2.0 provisioning APIs
- [references/rate-limits.md](references/rate-limits.md) - Rate limit details
- [references/qss.md](references/qss.md) - Quality of Service Subscription
Advanced
- [references/graphql.md](references/graphql.md) - GraphQL API (beta)
- [references/ai-companion.md](references/ai-companion.md) - AI features
- [references/authentication.md](references/authentication.md) - Auth reference
- [references/openapi.md](references/openapi.md) - OpenAPI specs, Postman, code generation
Additional API Domains
- [references/events.md](references/events.md) - Events and event platform APIs
- [references/scheduler.md](references/scheduler.md) - Zoom Scheduler APIs
- [references/tasks.md](references/tasks.md) - Tasks APIs
- [references/whiteboard.md](references/whiteboard.md) - Whiteboard APIs
- [references/video-management.md](references/video-management.md) - Video management APIs
- [references/video-sdk-api.md](references/video-sdk-api.md) - Video SDK REST APIs
- [references/marketplace-apps.md](references/marketplace-apps.md) - Marketplace app management
- [references/commerce.md](references/commerce.md) - Commerce and billing APIs
- [references/contact-center.md](references/contact-center.md) - Contact Center APIs
- [references/quality-management.md](references/quality-management.md) - Quality management APIs
- [references/workforce-management.md](references/workforce-management.md) - Workforce management APIs
- [references/healthcare.md](references/healthcare.md) - Healthcare APIs
- [references/auto-dialer.md](references/auto-dialer.md) - Auto dialer APIs
- [references/number-management.md](references/number-management.md) - Number management APIs
- [references/revenue-accelerator.md](references/revenue-accelerator.md) - Revenue Accelerator APIs
- [references/virtual-agent.md](references/virtual-agent.md) - Virtual Agent APIs
- [references/cobrowse-sdk-api.md](references/cobrowse-sdk-api.md) - Cobrowse SDK APIs
- [references/crc.md](references/crc.md) - Cloud Room Connector APIs
- [references/clips.md](references/clips.md) - Clips APIs
- [references/zoom-docs.md](references/zoom-docs.md) - Zoom docs and source references
Sample Repositories
Official (by Zoom)
| Type | Repository |
|---|---|
| OAuth Sample | oauth-sample-app |
| S2S OAuth Starter | server-to-server-oauth-starter-api |
| User OAuth | user-level-oauth-starter |
| S2S Token | server-to-server-oauth-token |
| Rivet Library | rivet-javascript |
| WebSocket Sample | websocket-js-sample |
| Webhook Sample | webhook-sample-node.js |
| Python S2S | server-to-server-python-sample |
Resources
- API Reference: https://developers.zoom.us/api-hub/
- GraphQL Playground: https://nws.zoom.us/graphql/playground
- Postman Collection: https://marketplace.zoom.us/docs/api-reference/postman
- Developer Forum: https://devforum.zoom.us/
- Changelog: https://developers.zoom.us/changelog/
- Status Page: https://status.zoom.us/
---
Need help? Start with Integrated Index section below for complete navigation.
---
Integrated Index
_This section was migrated from SKILL.md._
Quick Start Path
If you're new to the Zoom REST API, follow this order:
1. Run preflight checks first → RUNBOOK.md
2. Understand the API design → concepts/api-architecture.md
- Base URLs, regional endpoints,
mekeyword rules - Meeting ID vs UUID, double-encoding, time formats
3. Set up authentication → concepts/authentication-flows.md
- Server-to-Server OAuth (backend automation)
- User OAuth with PKCE (user-facing apps)
- Cross-reference: zoom-oauth
4. Create your first meeting → examples/meeting-lifecycle.md
- Full CRUD with curl and Node.js examples
- Webhook event integration
5. Handle rate limits → concepts/rate-limiting-strategy.md
- Plan-based limits, retry patterns, request queuing
6. Set up webhooks → examples/webhook-server.md
- CRC validation, signature verification, event handling
7. Troubleshoot issues → troubleshooting/common-issues.md
- Token refresh, pagination pitfalls, common gotchas
---
Documentation Structure
rest-api/
├── SKILL.md # Main skill overview + quick start
├── SKILL.md # This file - navigation guide
│
├── concepts/ # Core architectural concepts
│ ├── api-architecture.md # REST design, URLs, IDs, time formats
│ ├── authentication-flows.md # OAuth flows (S2S, User, PKCE, Device)
│ └── rate-limiting-strategy.md # Limits by plan, retry, queuing
│
├── examples/ # Complete working code
│ ├── meeting-lifecycle.md # Create→Update→Start→End→Delete
│ ├── user-management.md # CRUD users, pagination, bulk ops
│ ├── recording-pipeline.md # Download recordings via webhooks
│ ├── webhook-server.md # Express.js CRC + signature verification
│ └── graphql-queries.md # GraphQL queries, mutations, pagination
│
├── troubleshooting/ # Problem solving
│ ├── common-errors.md # HTTP codes, Zoom error codes table
│ └── common-issues.md # Rate limits, tokens, pagination pitfalls
│
└── references/ # 39 domain-specific reference files
├── authentication.md # Auth methods reference
├── meetings.md # Meeting endpoints
├── users.md # User management endpoints
├── webinars.md # Webinar endpoints
├── recordings.md # Cloud recording endpoints
├── reports.md # Reports & analytics
├── accounts.md # Account management
├── rate-limits.md # Rate limit details
├── graphql.md # GraphQL API (beta)
├── zoom-team-chat.md # Team Chat messaging
├── chatbot.md # Chatbot integration
├── phone.md # Zoom Phone
├── rooms.md # Zoom Rooms
├── calendar.md # Zoom Calendar
├── mail.md # Zoom Mail
├── ai-companion.md # AI features
├── openapi.md # OpenAPI specs
├── qss.md # Quality of Service
├── contact-center.md # Contact Center
├── events.md # Zoom Events
├── whiteboard.md # Whiteboard
├── clips.md # Zoom Clips
├── scheduler.md # Scheduler
├── scim2.md # SCIM 2.0
├── marketplace-apps.md # App management
├── zoom-video-sdk-api.md # Video SDK REST
└── ... (39 total files)---
By Use Case
I want to create and manage meetings
1. API Architecture - Base URL, time formats 2. Meeting Lifecycle - Full CRUD + webhook events 3. Meetings Reference - All endpoints, types, settings
I want to manage users programmatically
1. User Management - CRUD, pagination, bulk ops 2. Users Reference - Endpoints, user types, scopes
I want to download recordings automatically
1. Recording Pipeline - Webhook-triggered downloads 2. Recordings Reference - File types, download auth
I want to receive real-time events
1. Webhook Server - CRC validation, signature check 2. Cross-reference: zoom-webhooks for comprehensive webhook docs 3. Cross-reference: zoom-websockets for WebSocket events
I want to use GraphQL instead of REST
1. GraphQL Queries - Queries, mutations, pagination 2. GraphQL Reference - Available entities, scopes, rate limits
I want to set up authentication
1. Authentication Flows - All OAuth methods 2. Cross-reference: zoom-oauth for full OAuth implementation
I'm hitting rate limits
1. Rate Limiting Strategy - Limits by plan, strategies 2. Rate Limits Reference - Detailed tables 3. Common Issues - Practical solutions
I'm getting errors
1. Common Errors - Error code tables 2. Common Issues - Diagnostic workflow
I want to build webinars
1. Webinars Reference - Endpoints, types, registrants 2. Meeting Lifecycle - Similar patterns apply
I want to integrate Zoom Phone
1. Phone Reference - Phone API endpoints 2. Rate Limiting Strategy - Separate Phone rate limits
---
Most Critical Documents
1. API Architecture (FOUNDATION)
[concepts/api-architecture.md](concepts/api-architecture.md)
Essential knowledge before making any API call:
- Base URLs and regional endpoints
- The
mekeyword rules (different per app type!) - Meeting ID vs UUID double-encoding
- ISO 8601 time formats (UTC vs local)
- Download URL authentication
2. Rate Limiting Strategy (MOST COMMON PRODUCTION ISSUE)
[concepts/rate-limiting-strategy.md](concepts/rate-limiting-strategy.md)
Rate limits are per-account, shared across all apps:
- Free: 4/sec Light, 2/sec Medium, 1/sec Heavy
- Pro: 30/sec Light, 20/sec Medium, 10/sec Heavy
- Business+: 80/sec Light, 60/sec Medium, 40/sec Heavy
- Per-user: 100 meeting create/update per day
3. Meeting Lifecycle (MOST COMMON TASK)
[examples/meeting-lifecycle.md](examples/meeting-lifecycle.md)
Complete CRUD with webhook integration — the pattern most developers need first.
---
Key Learnings
Critical Discoveries:
1. JWT app type is deprecated — use Server-to-Server OAuth
- The JWT app type on Marketplace is deprecated, NOT JWT token signatures
- See: Authentication Flows
2. `me` keyword behaves differently by app type
- User OAuth: MUST use
me - S2S OAuth: MUST NOT use
me - See: API Architecture
3. Rate limiting is nuanced (don’t assume a single global rule)
- Limits can vary by endpoint and may be enforced at account/app/user levels
- Treat quotas as potentially shared across your account and implement backoff
- Monitor rate limit response headers (for example
X-RateLimit-Remaining) - See: Rate Limiting Strategy
4. 100 meeting creates per user per day
- This is a hard per-user limit, not related to rate limits
- Distribute across host users for bulk operations
- See: Rate Limiting Strategy
5. UUID double-encoding is required for certain UUIDs
- UUIDs starting with
/or containing//must be double-encoded - See: API Architecture
6. Pagination: use `next_page_token`, not `page_number`
page_numberis legacy and being phased outnext_page_tokenis the recommended approach- See: Common Issues
7. GraphQL is at `/v3/graphql`, not `/v2/`
- Single endpoint, cursor-based pagination
- Rate limits apply per-field (each field = one REST equivalent)
- See: GraphQL Queries
---
Quick Reference
"401 Unauthorized"
→ Authentication Flows - Token expired or wrong scopes
"429 Too Many Requests"
→ Rate Limiting Strategy - Check headers for reset time
"Invalid token" when using userId
→ API Architecture - User OAuth apps must use me
"How do I paginate results?"
→ Common Issues - Use next_page_token
"Webhooks not arriving"
→ Webhook Server - CRC validation required
"Recording download fails"
→ Recording Pipeline - Bearer auth + follow redirects
"How do I create a meeting?"
→ Meeting Lifecycle - Full working examples
---
Related Skills
| Skill | Use When |
|---|---|
| [zoom-oauth](../oauth/SKILL.md) | Implementing OAuth flows, token management |
| [zoom-webhooks](../webhooks/SKILL.md) | Deep webhook implementation, event catalog |
| [zoom-websockets](../websockets/SKILL.md) | WebSocket event streaming |
| [zoom-general](../general/SKILL.md) | Cross-product patterns, community repos |
---
Based on Zoom REST API v2 (current) and GraphQL v3 (beta)
Environment Variables
- See references/environment-variables.md for standardized
.envkeys and where to find each value.
API Architecture
Core design patterns for the Zoom REST API — base URLs, regional routing, identifiers, time formats, and request conventions.
Base URL
All requests use HTTPS with API version /v2 in the path:
https://api.zoom.us/v2/GraphQL uses a separate versioned endpoint:
https://api.zoom.us/v3/graphqlRegional Base URLs
The OAuth token response includes an api_url field indicating the user's data region. Use this for data residency compliance:
{
"access_token": "eyJ...",
"api_url": "https://api-eu.zoom.us"
}Construct your regional base URL by appending /v2/:
| Region | API URL | Base URL |
|---|---|---|
| Global (default) | https://api.zoom.us | https://api.zoom.us/v2 |
| Australia | https://api-au.zoom.us | https://api-au.zoom.us/v2 |
| Canada | https://api-ca.zoom.us | https://api-ca.zoom.us/v2 |
| European Union | https://api-eu.zoom.us | https://api-eu.zoom.us/v2 |
| India | https://api-in.zoom.us | https://api-in.zoom.us/v2 |
| Saudi Arabia | https://api-sa.zoom.us | https://api-sa.zoom.us/v2 |
| Singapore | https://api-sg.zoom.us | https://api-sg.zoom.us/v2 |
| United Kingdom | https://api-uk.zoom.us | https://api-uk.zoom.us/v2 |
| United States | https://api-us.zoom.us | https://api-us.zoom.us/v2 |
| Vanity account | https://{vanity}.zoom.us | https://{vanity}.zoom.us/v2 |
Important: The global URL https://api.zoom.us always works regardless of user region. Regional URLs are for compliance, not required.
Node.js — Dynamic Base URL from Token
async function getZoomClient(accountId, clientId, clientSecret) {
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const tokenRes = await fetch('https://zoom.us/oauth/token', {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `grant_type=account_credentials&account_id=${accountId}`
});
const tokenData = await tokenRes.json();
const baseUrl = tokenData.api_url
? `${tokenData.api_url}/v2`
: 'https://api.zoom.us/v2';
return {
accessToken: tokenData.access_token,
baseUrl,
async request(method, path, body = null) {
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Zoom API ${res.status}: ${err.message}`);
}
return res.json();
}
};
}The me Keyword
The me keyword substitutes for userId or accountId in API paths. Its behavior varies by app type:
| App Type | me Behavior | When to Use |
|---|---|---|
| User-level OAuth | Resolves to the authenticated user | MUST use — providing userId causes invalid token error |
| Server-to-Server OAuth | Not supported | MUST NOT use — provide actual userId or email |
| Account-level OAuth | Resolves to the user who installed the app | Can use either me or userId |
Examples
# User OAuth app — MUST use me
GET /v2/users/me
GET /v2/users/me/meetings
# S2S OAuth app — MUST use actual userId or email
GET /v2/users/abc123def
GET /v2/users/john@example.com
GET /v2/users/john@example.com/meetingsCommon Error
Using userId with a User-level OAuth token:
{
"code": 4700,
"message": "Invalid access token, does not contain scopes."
}Fix: Replace the userId with me.
Meeting ID vs UUID
- Meeting ID: Numeric identifier for the meeting. Reusable for recurring meetings. Expires 30 days after last use.
- UUID: Unique identifier for a specific meeting instance. Never expires. Generated per occurrence of recurring meetings.
When to Use Which
| Use Case | Use |
|---|---|
| Get a scheduled meeting | Meeting ID |
| Get a past meeting instance | UUID |
| Get recordings for a specific session | UUID |
| Report on a specific occurrence | UUID |
Double-Encoding UUIDs
UUIDs that begin with / or contain // must be double URL-encoded:
function encodeUUID(uuid) {
// Check if double-encoding is needed
if (uuid.startsWith('/') || uuid.includes('//')) {
return encodeURIComponent(encodeURIComponent(uuid));
}
return encodeURIComponent(uuid);
}
// UUID: /abcABC123==
// Single encode: %2FabcABC123%3D%3D
// Double encode: %252FabcABC123%253D%253D ← Required
const meetingUUID = '/abcABC123==';
const url = `https://api.zoom.us/v2/past_meetings/${encodeUUID(meetingUUID)}`;Python
from urllib.parse import quote
def encode_uuid(uuid_str):
if uuid_str.startswith('/') or '//' in uuid_str:
return quote(quote(uuid_str, safe=''), safe='')
return quote(uuid_str, safe='')
uuid = '/abcABC123=='
url = f'https://api.zoom.us/v2/past_meetings/{encode_uuid(uuid)}'Time Formats
Zoom API uses ISO 8601 with two variants:
| Format | Meaning | Example |
|---|---|---|
yyyy-MM-ddTHH:mm:ssZ | UTC time (Z suffix) | 2025-03-15T10:00:00Z |
yyyy-MM-ddTHH:mm:ss | Local time (no Z, uses timezone field) | 2025-03-15T10:00:00 |
Setting Meeting Time
{
"topic": "Team Meeting",
"type": 2,
"start_time": "2025-03-15T10:00:00",
"timezone": "America/Los_Angeles",
"duration": 60
}Or using UTC directly:
{
"topic": "Team Meeting",
"type": 2,
"start_time": "2025-03-15T17:00:00Z",
"duration": 60
}Note: Some Report APIs only accept UTC format. Always check the endpoint reference for the accepted format.
Date-Only Parameters
Some endpoints (e.g., recordings list) use YYYY-MM-DD format:
GET /v2/users/me/recordings?from=2025-01-01&to=2025-01-31Download URLs
Recording download_url values in API responses and webhook payloads are dynamically generated. They require authentication:
Authentication Methods
1. Bearer token in Authorization header (recommended):
curl -L -H "Authorization: Bearer ACCESS_TOKEN" \
"https://zoom.us/rec/archive/download/xyz"2. `download_access_token` from webhook payload (for webhook-triggered downloads):
curl -L -H "Authorization: Bearer DOWNLOAD_ACCESS_TOKEN" \
"https://zoom.us/rec/archive/download/xyz"Follow Redirects
Download URLs may return HTTP 301/302 redirects. Always follow redirects:
// Node.js — fetch follows redirects by default
const response = await fetch(downloadUrl, {
headers: { 'Authorization': `Bearer ${accessToken}` },
redirect: 'follow'
});
const fileBuffer = await response.arrayBuffer();# Python — requests follows redirects by default
import requests
response = requests.get(
download_url,
headers={'Authorization': f'Bearer {access_token}'},
allow_redirects=True,
stream=True
)
with open('recording.mp4', 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)Personal Meeting ID (PMI)
Users can create meetings with their PMI. The API returns a unique meeting ID in the response, but webhook events still reference the PMI. Use the PMI when passing IDs to API endpoints for PMI-based meetings.
Shared Access Permissions
Users with Schedule Privilege or role-based access can act on behalf of other users. If your app accesses resources of a user other than the one who installed the app, that user must have authorized shared access permissions.
Error when shared access is not granted:
{
"code": 403,
"message": "authenticated user has not permitted access to the targeted resource"
}Resolution: Direct the user to enable shared access permissions in their Zoom settings. See Zoom Help Center for the user-facing instructions.
Email Address Display Rules
External participant emails are only shown if:
- The participant entered their email during registration
- The host provided the email via calendar integration, authentication exception, or breakout room assignment
- A CSV was imported for webinar panelists/attendees
High API Failure Rates
If your app has a consistently high error-to-request ratio, Zoom may disable it. Build robust error handling and graceful retry logic.
Request Authentication
All API requests require a Bearer token in the Authorization header:
Authorization: Bearer {access_token}Full auth implementation: See Authentication Flows or the [zoom-oauth](../../oauth/SKILL.md) skill.
Resources
- Using Zoom APIs: https://developers.zoom.us/docs/api/using-zoom-apis/
- API Reference: https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/
Authentication Flows
All Zoom REST API requests require OAuth 2.0 authentication. This guide covers all supported OAuth flows and when to use each.
Complete OAuth implementation guide: See the [zoom-oauth](../../oauth/SKILL.md) skill for full code examples, token storage, and production patterns.
Flow Selection
| Flow | Use Case | User Interaction | Token Lifetime |
|---|---|---|---|
| Server-to-Server OAuth | Backend automation, bots, integrations | None | 1 hour |
| Authorization Code | User-facing web apps | User consent flow | 1 hour (refresh: 15 years) |
| Authorization Code + PKCE | SPAs, mobile apps | User consent flow | 1 hour (refresh: 15 years) |
| Device Code | TV/IoT devices, CLI tools | User enters code on separate device | 1 hour (refresh: 15 years) |
| ~~JWT~~ | ~~Legacy~~ | ~~None~~ | DEPRECATED — migrate to S2S OAuth |
Server-to-Server OAuth (Recommended for Backend)
No user interaction required. Best for automation, scheduled tasks, and backend services.
Setup
1. Go to Zoom App Marketplace → Develop → Build App 2. Select Server-to-Server OAuth 3. Note: Account ID, Client ID, Client Secret 4. Add required scopes (e.g., meeting:write:admin, user:read:admin)
Get Access Token
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=account_credentials&account_id=ACCOUNT_ID"Response
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "meeting:read meeting:write user:read",
"api_url": "https://api.zoom.us"
}Node.js — Token Manager with Auto-Refresh
class ZoomS2SAuth {
constructor(accountId, clientId, clientSecret) {
this.accountId = accountId;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.token = null;
this.tokenExpiry = 0;
}
async getAccessToken() {
// Return cached token if valid (with 60s buffer)
if (this.token && Date.now() < this.tokenExpiry - 60000) {
return this.token;
}
const credentials = Buffer.from(
`${this.clientId}:${this.clientSecret}`
).toString('base64');
const response = await fetch('https://zoom.us/oauth/token', {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `grant_type=account_credentials&account_id=${this.accountId}`
});
if (!response.ok) {
const err = await response.json();
throw new Error(`Token error: ${err.error} - ${err.reason}`);
}
const data = await response.json();
this.token = data.access_token;
this.tokenExpiry = Date.now() + (data.expires_in * 1000);
return this.token;
}
async request(method, path, body = null) {
const token = await this.getAccessToken();
const response = await fetch(`https://api.zoom.us/v2${path}`, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(`Zoom API ${response.status}: ${JSON.stringify(err)}`);
}
// Some endpoints return 204 No Content
if (response.status === 204) return null;
return response.json();
}
}
// Usage
const zoom = new ZoomS2SAuth(
process.env.ZOOM_ACCOUNT_ID,
process.env.ZOOM_CLIENT_ID,
process.env.ZOOM_CLIENT_SECRET
);
const users = await zoom.request('GET', '/users?page_size=300');
const meeting = await zoom.request('POST', '/users/user@example.com/meetings', {
topic: 'API Meeting', type: 2, duration: 30
});Python — Token Manager
import requests
import time
from base64 import b64encode
class ZoomS2SAuth:
def __init__(self, account_id, client_id, client_secret):
self.account_id = account_id
self.client_id = client_id
self.client_secret = client_secret
self.token = None
self.token_expiry = 0
def get_access_token(self):
if self.token and time.time() < self.token_expiry - 60:
return self.token
credentials = b64encode(
f'{self.client_id}:{self.client_secret}'.encode()
).decode()
response = requests.post(
'https://zoom.us/oauth/token',
headers={
'Authorization': f'Basic {credentials}',
'Content-Type': 'application/x-www-form-urlencoded'
},
data=f'grant_type=account_credentials&account_id={self.account_id}'
)
response.raise_for_status()
data = response.json()
self.token = data['access_token']
self.token_expiry = time.time() + data['expires_in']
return self.token
def request(self, method, path, json_data=None):
token = self.get_access_token()
response = requests.request(
method,
f'https://api.zoom.us/v2{path}',
headers={'Authorization': f'Bearer {token}'},
json=json_data
)
response.raise_for_status()
return response.json() if response.content else NoneUser OAuth (Authorization Code)
For apps that act on behalf of individual Zoom users.
Flow
1. User clicks "Connect to Zoom"
2. Redirect to: https://zoom.us/oauth/authorize?response_type=code&client_id=XXX&redirect_uri=YYY&state=ZZZ
3. User grants permission
4. Zoom redirects to callback: https://yourapp.com/callback?code=AUTH_CODE&state=ZZZ
5. Exchange code for tokens
6. Use access_token for API calls
7. Refresh when expiredExchange Code for Token
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https://yourapp.com/callback"Refresh Token
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token&refresh_token=REFRESH_TOKEN"Important: me Keyword
User OAuth apps must use me instead of userId in API paths:
# CORRECT for user OAuth
GET /v2/users/me/meetings
# WRONG for user OAuth — will return "Invalid access token"
GET /v2/users/abc123/meetingsCommon Scopes
| Scope | Description |
|---|---|
user:read | Read user profile |
user:read:admin | Read all users (admin) |
user:write:admin | Manage all users (admin) |
meeting:read | Read meeting data |
meeting:write | Create/update meetings |
meeting:write:admin | Create/update any user's meetings |
recording:read | Access recordings |
recording:write | Manage recordings |
webinar:read | Read webinar data |
webinar:write | Manage webinars |
report:read:admin | View reports |
Best practice: Request only the scopes you need. Fewer scopes = less user friction and faster app approval.
Token Storage Best Practices
// DO: Encrypt tokens at rest
const encrypted = encrypt(accessToken, process.env.ENCRYPTION_KEY);
await db.tokens.upsert({ userId, encrypted, expiresAt });
// DO: Use httpOnly secure cookies for web apps
res.cookie('zoom_session', sessionId, {
httpOnly: true, secure: true, sameSite: 'strict', maxAge: 3600000
});
// DON'T: Store tokens in localStorage or log them
localStorage.setItem('zoom_token', token); // INSECURE
console.log('Token:', accessToken); // LEAKS CREDENTIALSError Handling
| Error | Cause | Solution |
|---|---|---|
invalid_grant | Expired/used auth code or refresh token | Restart OAuth flow or re-authenticate |
invalid_client | Wrong client ID or secret | Verify credentials |
invalid_scope | Scope not approved for your app | Check app scopes in Marketplace |
access_denied | User denied permission | Handle gracefully in UI |
try {
const token = await refreshAccessToken(refreshToken);
} catch (error) {
if (error.response?.data?.error === 'invalid_grant') {
// Refresh token revoked or expired — re-authenticate
redirectToOAuthFlow();
}
}Migration from JWT (Deprecated)
The JWT app type on Zoom Marketplace is deprecated. This does not affect JWT token signatures used elsewhere (e.g., Video SDK).
Steps: 1. Create a Server-to-Server OAuth app 2. Request the same scopes 3. Replace JWT token generation with OAuth token endpoint 4. Test all endpoints 5. Delete the JWT app
Resources
- OAuth Guide: https://developers.zoom.us/docs/integrations/oauth/
- S2S OAuth: https://developers.zoom.us/docs/internal-apps/s2s-oauth/
- Scopes Reference: https://developers.zoom.us/docs/integrations/oauth-scopes/
- Full OAuth Skill: See [zoom-oauth](../../oauth/SKILL.md)
Meeting URLs vs Meeting SDK Joining
Forum confusion pattern:
- “How do I generate a Zoom meeting URL server-side?”
- “How do I join a meeting via API?”
- “Can I use
join_urlwith Meeting SDK?”
REST API: What You Get
When you create a meeting via REST API, you typically get:
join_url: for participants to join using Zoom clients/web join linksstart_url: for the host (often time-limited, and tied to the host context)id/ meeting number: the meeting identifier
Meeting SDK: What It Uses
Meeting SDK integrations generally use:
meetingNumber(meeting id)- Meeting SDK signature (generated server-side)
role(0 join, 1 start)- passcode (if required)
So: you usually do not “feed join_url into Meeting SDK”. You use the meeting number + SDK signature.
“Join via API” Clarification
The REST API does not “join” a meeting as a client. If the goal is to build an embedded or automated participant, you’re typically looking at:
- Meeting SDK (embed/join/start flows)
- or bot-style patterns (Linux Meeting SDK, or RTMS for media access), depending on the end goal
Rate Limiting Strategy
Zoom API rate limits by plan, category, and strategies for handling them in production.
Rate Limits by Account Plan
Rate limits are per-account (shared by all users and all apps on the account):
Main REST API
| Category | Free | Pro | Business+ |
|---|---|---|---|
| Light | 4/sec, 6,000/day | 30/sec | 80/sec |
| Medium | 2/sec, 2,000/day | 20/sec | 60/sec |
| Heavy | 1/sec, 1,000/day | 10/sec* | 40/sec* |
| Resource-Intensive | 10/min, 30,000/day | 10/min* | 20/min* |
*\ Combined daily limits:**
- Pro: 30,000/day (Heavy + Resource-Intensive shared)
- Business+: 60,000/day (Heavy + Resource-Intensive shared)
Business+ includes: Business, Education, Enterprise, and Partners.
Zoom Phone API
| Category | Pro | Business+ |
|---|---|---|
| Light | 20/sec | 40/sec |
| Medium | 10/sec | 20/sec |
| Heavy | 5/sec, 15,000/day* | 10/sec, 30,000/day* |
| Resource-Intensive | 5/min, 15,000/day* | 10/min, 30,000/day* |
*\ Daily limit shared** between Heavy and Resource-Intensive.
Zoom Contact Center API
| Category | Pro | Business+ |
|---|---|---|
| Light | 20/sec | 40/sec |
| Medium | 10/sec | 20/sec |
| Heavy | 5/sec, 15,000/day* | 10/sec, 30,000/day* |
*\ Daily limit shared** with Resource-Intensive APIs.
Video SDK Account Rate Limits
| Plan | Uses Limits |
|---|---|
| Pay As You Go (Deprecated) | Pro |
| Annual Prepay Monthly Usage | Pro |
| All other plans | Business+ |
Endpoint Category Examples
| Light | Medium | Heavy |
|---|---|---|
| Get A Meeting | Create Meeting | Get Daily Usage Report |
| Get Meeting Recordings | List All Recordings | List Devices |
| Add Meeting Registrant | Get Past Meeting Participants | — |
| Update A Meeting | List Meetings | — |
Per-User Daily Limits
These are separate from account-level rate limits:
| Operation | Limit | Reset |
|---|---|---|
| Meeting/Webinar Create/Update | 100/day per user | 00:00 UTC |
| Registrant Addition | 3/day per registrant | 00:00 UTC |
| Registrant Status Updates | 10/day per registrant | 00:00 UTC |
The 100/day limit applies to all Meeting/Webinar IDs hosted by a specific user. To bulk-create meetings, distribute across multiple host users.
Concurrent Request Limits (Lock-Key)
Zoom enforces single-concurrency on certain resource operations:
| Scenario | Behavior |
|---|---|
| Multiple DELETE on same userId | Only 1 concurrent DELETE allowed |
POST to /v2/users | Blocks GET/PATCH/PUT/DELETE until complete |
Error:
{
"code": 429,
"message": "Too many concurrent requests. A request to disassociate this user has already been made."
}Response Headers
Every API response includes rate limit information:
| Header | Description |
|---|---|
X-RateLimit-Category | Light, Medium, Heavy, or Resource-intensive |
X-RateLimit-Type | QPS (per-second) or Daily-limit |
X-RateLimit-Limit | Max requests in current window |
X-RateLimit-Remaining | Requests remaining |
X-RateLimit-Reset | Unix timestamp when per-second limit resets |
Retry-After | ISO 8601 datetime when daily limit resets |
Example — Normal Response
X-RateLimit-Category: Medium
X-RateLimit-Type: QPS
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 55Example — Per-Second Rate Limited
HTTP/1.1 429 Too Many Requests
X-RateLimit-Category: Light
X-RateLimit-Type: QPS
X-RateLimit-Limit: 80
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312800Example — Daily Rate Limited
HTTP/1.1 429 Too Many Requests
X-RateLimit-Category: Heavy
X-RateLimit-Type: Daily-limit
X-RateLimit-Limit: 60000
X-RateLimit-Remaining: 0
Retry-After: 2025-01-20T00:00:00ZStrategy 1: Exponential Backoff with Jitter
The simplest retry strategy for handling 429 responses:
async function callZoomAPI(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
// Check for daily limit (Retry-After header)
const retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
const waitMs = new Date(retryAfter) - Date.now();
console.warn(`Daily limit hit. Retry after: ${retryAfter}`);
if (waitMs > 0 && waitMs < 86400000) {
await sleep(waitMs);
continue;
}
throw new Error(`Daily rate limit hit. Retry after ${retryAfter}`);
}
// Per-second limit — exponential backoff with jitter
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = baseDelay * 0.2 * Math.random();
const delay = baseDelay + jitter;
console.warn(`Rate limited. Retrying in ${Math.round(delay)}ms (attempt ${attempt + 1})`);
await sleep(delay);
continue;
}
return response;
}
throw new Error('Max retries exceeded for Zoom API');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Strategy 2: Proactive Throttling
Monitor remaining quota and slow down before hitting limits:
async function throttledRequest(url, options) {
const response = await fetch(url, options);
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '999');
const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '999');
const category = response.headers.get('X-RateLimit-Category');
// Proactive throttling when under 10% quota
if (remaining < limit * 0.1) {
const resetTs = response.headers.get('X-RateLimit-Reset');
if (resetTs) {
const waitMs = (parseInt(resetTs) * 1000) - Date.now();
if (waitMs > 0 && waitMs < 10000) {
console.warn(`[${category}] ${remaining}/${limit} remaining — throttling ${waitMs}ms`);
await sleep(waitMs);
}
} else {
await sleep(1000);
}
}
return response;
}Strategy 3: Request Queue (High-Volume)
For applications making many concurrent requests:
class ZoomRateLimitedQueue {
constructor(requestsPerSecond = 10, minDelayMs = 100) {
this.queue = [];
this.running = 0;
this.maxConcurrent = requestsPerSecond;
this.minDelayMs = minDelayMs;
this.processing = false;
}
async add(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.process();
});
}
async process() {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0) {
if (this.running >= this.maxConcurrent) {
await sleep(this.minDelayMs);
continue;
}
const { requestFn, resolve, reject } = this.queue.shift();
this.running++;
requestFn()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--;
});
await sleep(this.minDelayMs);
}
this.processing = false;
}
}
// Usage — process 10 requests/sec max
const queue = new ZoomRateLimitedQueue(10, 100);
const userIds = ['user1', 'user2', 'user3', /* ... */];
const results = await Promise.all(
userIds.map(id =>
queue.add(() => zoom.request('GET', `/users/${id}`))
)
);Best Practices
1. Cache GET Responses
const cache = new Map();
async function cachedGet(path, ttlMs = 60000) {
const cached = cache.get(path);
if (cached && Date.now() - cached.time < ttlMs) {
return cached.data;
}
const data = await zoom.request('GET', path);
cache.set(path, { data, time: Date.now() });
return data;
}2. Use Webhooks Instead of Polling
// DON'T: Poll for meeting status changes
setInterval(async () => {
const meetings = await zoom.request('GET', `/users/${userId}/meetings`);
}, 60000);
// DO: Receive webhook events
app.post('/webhook', (req, res) => {
handleEvent(req.body);
res.status(200).send();
});See [zoom-webhooks](../../webhooks/SKILL.md) for webhook implementation.
3. Use List Endpoints with Pagination
// DON'T: Fetch users one by one (N API calls)
for (const id of userIds) {
const user = await zoom.request('GET', `/users/${id}`);
}
// DO: Fetch in bulk (1 API call per page)
const allUsers = await zoom.request('GET', '/users?page_size=300');4. Distribute Bulk Creates Across Users
// Avoid hitting the 100/day per-user limit
const hosts = ['host1@co.com', 'host2@co.com', 'host3@co.com'];
let hostIndex = 0;
for (const meeting of meetingsToCreate) {
const host = hosts[hostIndex % hosts.length];
await zoom.request('POST', `/users/${host}/meetings`, meeting);
hostIndex++;
await sleep(100); // Prevent per-second burst
}5. Use QSS for Quality Data
For Quality of Service data, use QSS (push-based) instead of polling Reports API:
- Streams telemetry via webhooks/WebSocket
- Pushes data 4-6 times per minute
- Drastically reduces API call volume
Common Gotchas
| Issue | Solution |
|---|---|
| 429 on first request of the day | Another app on account used quota |
| Different limits than documented | Check account type (Free/Pro/Business+) |
| Meeting create fails at 100/day | Per-user limit — distribute across hosts |
| Concurrent DELETE errors | Serialize DELETE operations on same user |
| Daily limit hit unexpectedly | Heavy + Resource-Intensive share quota |
Resources
- Rate Limits Documentation: https://developers.zoom.us/docs/api/rest/rate-limits/
- Detailed Reference: references/rate-limits.md
GraphQL Queries (Zoom)
Use this when a forum question is really about "how do I fetch X without calling 10 REST endpoints".
Practical Guidance
- Treat GraphQL as an alternative query surface. Not all REST resources are available.
- Auth is still OAuth; the most common failure mode is missing scopes.
Pitfalls Seen In Forum Threads
- Confusing GraphQL "cursor" pagination with REST
next_page_token. - Assuming GraphQL replaces webhooks. It does not.
Meeting Lifecycle - Complete CRUD with Webhook Integration
Complete working examples for the full meeting lifecycle: Create → Update → Start → End → Delete, with webhook event integration.
Prerequisites
- Server-to-Server OAuth token (see Authentication Flows)
- Scopes:
meeting:write,meeting:read(minimum) - Base URL:
https://api.zoom.us/v2
Step 1: Create a Meeting
Instant Meeting (No Fixed Time)
curl -X POST "https://api.zoom.us/v2/users/me/meetings" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"topic": "Quick Team Sync",
"type": 1,
"settings": {
"join_before_host": false,
"waiting_room": true,
"approval_type": 2
}
}'Scheduled Meeting
curl -X POST "https://api.zoom.us/v2/users/me/meetings" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type": application/json" \
-d '{
"topic": "Q1 Planning Meeting",
"type": 2,
"start_time": "2025-03-15T10:00:00Z",
"duration": 60,
"timezone": "America/New_York",
"agenda": "Discuss Q1 goals and milestones",
"settings": {
"host_video": true,
"participant_video": false,
"join_before_host": false,
"waiting_room": true,
"mute_upon_entry": true,
"approval_type": 2,
"auto_recording": "cloud",
"alternative_hosts": "alt.host@example.com"
}
}'Node.js Example
async function createMeeting(accessToken, meetingData) {
const response = await fetch('https://api.zoom.us/v2/users/me/meetings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(meetingData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to create meeting: ${error.message}`);
}
return await response.json();
}
// Usage
const meetingData = {
topic: 'Team Standup',
type: 2, // Scheduled
start_time: '2025-03-15T14:00:00Z',
duration: 30,
settings: {
join_before_host: false,
waiting_room: true
}
};
const meeting = await createMeeting(accessToken, meetingData);
console.log('Meeting created:', meeting.id);
console.log('Join URL:', meeting.join_url);Response
{
"id": 93123456789,
"uuid": "xyzAbC1234==",
"host_id": "abc123def456",
"topic": "Q1 Planning Meeting",
"type": 2,
"start_time": "2025-03-15T10:00:00Z",
"duration": 60,
"timezone": "America/New_York",
"created_at": "2025-02-09T12:30:00Z",
"join_url": "https://zoom.us/j/93123456789",
"start_url": "https://zoom.us/s/93123456789?zak=...",
"settings": {
"host_video": true,
"participant_video": false,
"waiting_room": true,
"auto_recording": "cloud"
}
}Meeting Types
| Type | Value | Description |
|---|---|---|
| Instant | 1 | Start immediately, no fixed time |
| Scheduled | 2 | Fixed date/time |
| Recurring (no fixed time) | 3 | PMI meetings |
| Recurring (fixed time) | 8 | Series with schedule |
Step 2: Update a Meeting
Update Meeting Details
curl -X PATCH "https://api.zoom.us/v2/meetings/93123456789" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"topic": "Q1 Planning - Updated",
"start_time": "2025-03-15T14:00:00Z",
"duration": 90,
"settings": {
"waiting_room": false
}
}'Node.js Example
async function updateMeeting(accessToken, meetingId, updates) {
const response = await fetch(`https://api.zoom.us/v2/meetings/${meetingId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to update meeting: ${error.message}`);
}
// PATCH returns 204 No Content on success
return response.status === 204;
}
// Usage
const updates = {
topic: 'Q1 Planning - Updated Agenda',
duration: 90
};
await updateMeeting(accessToken, 93123456789, updates);
console.log('Meeting updated successfully');Partial Updates
You only need to include fields you want to change:
// Only update topic
await updateMeeting(accessToken, meetingId, {
topic: 'New Topic'
});
// Only update settings
await updateMeeting(accessToken, meetingId, {
settings: {
waiting_room: true,
mute_upon_entry: true
}
});Step 3: Get Meeting Details
curl "https://api.zoom.us/v2/meetings/93123456789" \
-H "Authorization: Bearer ACCESS_TOKEN"Node.js Example
async function getMeeting(accessToken, meetingId) {
const response = await fetch(
`https://api.zoom.us/v2/meetings/${meetingId}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to get meeting: ${error.message}`);
}
return await response.json();
}
// Usage
const meeting = await getMeeting(accessToken, 93123456789);
console.log('Meeting:', meeting.topic);
console.log('Start time:', meeting.start_time);
console.log('Join URL:', meeting.join_url);Step 4: List User's Meetings
curl "https://api.zoom.us/v2/users/me/meetings?type=scheduled&page_size=30" \
-H "Authorization: Bearer ACCESS_TOKEN"Node.js with Pagination
async function listAllMeetings(accessToken, userId = 'me') {
let allMeetings = [];
let nextPageToken = '';
while (true) {
const params = new URLSearchParams({
type: 'scheduled',
page_size: 300,
...(nextPageToken && { next_page_token: nextPageToken })
});
const response = await fetch(
`https://api.zoom.us/v2/users/${userId}/meetings?${params}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to list meetings: ${error.message}`);
}
const data = await response.json();
allMeetings = allMeetings.concat(data.meetings);
nextPageToken = data.next_page_token;
if (!nextPageToken) break;
}
return allMeetings;
}
// Usage
const meetings = await listAllMeetings(accessToken);
console.log(`Found ${meetings.length} meetings`);
meetings.forEach(m => console.log(`- ${m.topic} (${m.start_time})`));Meeting List Types
| Type | Value | Description |
|---|---|---|
| Scheduled | scheduled | Future meetings |
| Live | live | Currently active |
| Upcoming | upcoming | Within next 30 days |
Step 5: Delete a Meeting
curl -X DELETE "https://api.zoom.us/v2/meetings/93123456789" \
-H "Authorization: Bearer ACCESS_TOKEN"Node.js Example
async function deleteMeeting(accessToken, meetingId, options = {}) {
const params = new URLSearchParams();
// Optional: Cancel single occurrence of recurring meeting
if (options.occurrenceId) {
params.append('occurrence_id', options.occurrenceId);
}
// Optional: Send cancellation email
if (options.scheduleForReminder !== undefined) {
params.append('schedule_for_reminder', options.scheduleForReminder);
}
const url = `https://api.zoom.us/v2/meetings/${meetingId}${params.toString() ? '?' + params.toString() : ''}`;
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to delete meeting: ${error.message}`);
}
// DELETE returns 204 No Content on success
return response.status === 204;
}
// Usage
await deleteMeeting(accessToken, 93123456789);
console.log('Meeting deleted successfully');Webhook Integration
To receive real-time events for meeting lifecycle, set up webhooks. See Webhook Server Example for full implementation.
Key Meeting Events
| Event | When It Fires |
|---|---|
meeting.created | Meeting is created |
meeting.updated | Meeting details changed |
meeting.deleted | Meeting is deleted |
meeting.started | Meeting begins |
meeting.ended | Meeting ends |
meeting.participant_joined | Participant joins |
meeting.participant_left | Participant leaves |
recording.completed | Cloud recording finishes processing |
Webhook Payload Example
Event: meeting.started
{
"event": "meeting.started",
"event_ts": 1707486720000,
"payload": {
"account_id": "abc123",
"object": {
"id": "93123456789",
"uuid": "xyzAbC1234==",
"host_id": "def456",
"topic": "Q1 Planning Meeting",
"type": 2,
"start_time": "2025-03-15T14:00:00Z",
"duration": 60,
"timezone": "America/New_York"
}
}
}Webhook Handler Example
// Express.js webhook endpoint
app.post('/webhook', express.json(), (req, res) => {
const { event, payload } = req.body;
switch (event) {
case 'meeting.started':
console.log(`Meeting started: ${payload.object.topic}`);
// Trigger recording, send notifications, etc.
break;
case 'meeting.ended':
console.log(`Meeting ended: ${payload.object.topic}`);
// Process analytics, download recordings, etc.
break;
case 'recording.completed':
console.log(`Recording ready: ${payload.object.topic}`);
// Download recording (see recording-pipeline.md)
break;
default:
console.log(`Unhandled event: ${event}`);
}
// Respond with 200 to acknowledge receipt
res.status(200).send();
});Complete Lifecycle Workflow
Automated Meeting Management
class MeetingManager {
constructor(accessToken) {
this.accessToken = accessToken;
}
async createScheduledMeeting(topic, startTime, duration = 60) {
const meetingData = {
topic,
type: 2,
start_time: startTime,
duration,
settings: {
join_before_host: false,
waiting_room: true,
auto_recording: 'cloud'
}
};
const meeting = await this.createMeeting(meetingData);
console.log(`Created meeting: ${meeting.id}`);
return meeting;
}
async updateMeetingTime(meetingId, newStartTime) {
await this.updateMeeting(meetingId, {
start_time: newStartTime
});
console.log(`Updated meeting ${meetingId} start time`);
}
async cancelMeeting(meetingId) {
await this.deleteMeeting(meetingId, {
schedule_for_reminder: true // Send cancellation email
});
console.log(`Cancelled meeting ${meetingId}`);
}
async getUpcomingMeetings() {
const meetings = await this.listAllMeetings();
const now = new Date();
return meetings.filter(m => {
const startTime = new Date(m.start_time);
return startTime > now;
});
}
// Helper methods (implementations from above examples)
async createMeeting(data) { /* ... */ }
async updateMeeting(id, updates) { /* ... */ }
async deleteMeeting(id, options) { /* ... */ }
async listAllMeetings() { /* ... */ }
}
// Usage
const manager = new MeetingManager(accessToken);
// Create meeting
const meeting = await manager.createScheduledMeeting(
'Team Standup',
'2025-03-15T10:00:00Z',
30
);
// Update if needed
await manager.updateMeetingTime(meeting.id, '2025-03-15T14:00:00Z');
// Get all upcoming meetings
const upcoming = await manager.getUpcomingMeetings();
console.log(`${upcoming.length} upcoming meetings`);
// Cancel if needed
await manager.cancelMeeting(meeting.id);Common Patterns
Recurring Meeting Series
const recurringMeeting = {
topic: 'Weekly Team Sync',
type: 8, // Recurring with fixed time
start_time: '2025-03-15T10:00:00Z',
duration: 30,
recurrence: {
type: 2, // Weekly
repeat_interval: 1,
weekly_days: '1,3,5', // Monday, Wednesday, Friday
end_times: 20 // 20 occurrences
}
};
const meeting = await createMeeting(accessToken, recurringMeeting);Meeting with Registration
const meetingWithRegistration = {
topic: 'Product Demo',
type: 2,
start_time: '2025-03-15T14:00:00Z',
duration: 60,
settings: {
approval_type: 0, // Automatic approval
registration_type: 1, // Attendees register once
meeting_authentication: false
}
};
const meeting = await createMeeting(accessToken, meetingWithRegistration);
console.log('Registration URL:', meeting.registration_url);PMI Meeting
const pmiMeeting = {
topic: 'My Personal Room',
type: 3, // Recurring with no fixed time (PMI)
settings: {
use_pmi: true,
join_before_host: true
}
};
const meeting = await createMeeting(accessToken, pmiMeeting);Error Handling
Per-User Daily Limit
Meeting/webinar create/update operations are limited to 100 per day per user (resets at 00:00 UTC).
async function createMeetingWithRetry(accessToken, meetingData, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await createMeeting(accessToken, meetingData);
} catch (error) {
if (error.message.includes('Too many requests')) {
console.log(`Hit per-user limit. Attempt ${attempt}/${maxRetries}`);
if (attempt < maxRetries) {
await sleep(5000); // Wait 5 seconds
continue;
}
}
throw error;
}
}
}Validation Errors
try {
await createMeeting(accessToken, meetingData);
} catch (error) {
if (error.message.includes('Invalid field')) {
console.error('Validation error:', error);
// Check start_time format, type value, etc.
} else if (error.message.includes('User does not exist')) {
console.error('Invalid userId');
} else {
throw error;
}
}Related Documentation
- [API Architecture](../concepts/api-architecture.md) - Base URLs,
mekeyword, time formats - [Authentication Flows](../concepts/authentication-flows.md) - Get access tokens
- [Webhook Server](webhook-server.md) - Receive meeting events
- [Recording Pipeline](recording-pipeline.md) - Download meeting recordings
- [Meetings Reference](../references/meetings.md) - Complete endpoint documentation
Resources
Recording Pipeline (Webhook -> Download -> Store)
Goal: automatically ingest cloud recordings after meetings end.
High-Level Steps
1. Subscribe to recording-related webhooks (e.g. recording.completed). 2. On webhook: fetch recording files via the recordings endpoints. 3. Download files using authenticated requests (often download_url requires an Authorization header). 4. Store in your system (S3/GCS/etc) and track status.
Common Pitfalls
- Following
download_urlwithout attaching a bearer token. - Not handling redirect responses from
download_url. - Assuming recording is available immediately after meeting ends (processing delays).
User Management (Create/List/Update)
This doc targets the common "how do I list users / create users / search users" forum clusters.
Common Tasks
- List users with pagination
- Create user (custCreate / SSO users vary by account settings)
- Update user type/license
- Deactivate/delete users
Pitfalls
- Page size vs plan limits.
- Admin-only scopes needed for many user operations.
- "Search by first name" is not always supported as a direct filter; you may need to page and filter client-side.
Webhook Server - Express.js with CRC Validation and Signature Verification
Production-ready webhook server implementation for receiving Zoom webhook events with CRC (Challenge-Response Check) validation and HMAC signature verification.
For comprehensive webhook documentation, see the [webhooks skill](../../webhooks/SKILL.md).
Quick Start
1. Install Dependencies
npm install express body-parser crypto2. Basic Webhook Server
const express = require('express');
const crypto = require('crypto');
const app = express();
// Zoom webhook secret token (from your app's Feature page)
const WEBHOOK_SECRET_TOKEN = process.env.ZOOM_WEBHOOK_SECRET;
// Parse JSON bodies
app.use(express.json());
// Webhook endpoint
app.post('/webhook', (req, res) => {
const { event, payload } = req.body;
// Handle CRC validation (Challenge-Response Check)
if (event === 'endpoint.url_validation') {
return handleCRC(req, res);
}
// Verify signature
if (!verifySignature(req)) {
console.error('Invalid signature');
return res.status(401).send('Unauthorized');
}
// Handle events
handleEvent(event, payload);
// Always respond with 200 within 3 seconds
res.status(200).send();
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});CRC (Challenge-Response Check) Validation
When you add a webhook URL or make changes, Zoom sends a validation request. You must respond within 3 seconds.
CRC Flow
1. Zoom sends POST with event: "endpoint.url_validation" 2. Your server hashes the plainToken using your webhook secret 3. Respond with JSON containing both plainToken and encryptedToken
Implementation
function handleCRC(req, res) {
const { plainToken } = req.body.payload;
// Hash the plainToken with HMAC-SHA256
const encryptedToken = crypto
.createHmac('sha256', WEBHOOK_SECRET_TOKEN)
.update(plainToken)
.digest('hex');
// Respond within 3 seconds
res.status(200).json({
plainToken,
encryptedToken
});
console.log('CRC validation successful');
}CRC Request Example
{
"event": "endpoint.url_validation",
"payload": {
"plainToken": "qgg8vlvZRS6UYooatFL8Aw"
},
"event_ts": 1654503849680
}CRC Response Example
{
"plainToken": "qgg8vlvZRS6UYooatFL8Aw",
"encryptedToken": "23a89b634c017e5364a1c8d9c8ea909b60dd5599e2bb04bb1558d9c3a121faa5"
}Signature Verification
Verify that webhook requests actually come from Zoom by checking the HMAC signature.
Signature Verification Flow
1. Extract x-zm-signature and x-zm-request-timestamp headers 2. Construct message: v0:{timestamp}:{body} 3. Hash message with HMAC-SHA256 using your webhook secret 4. Prepend v0= to the hash 5. Compare with x-zm-signature header
Implementation
function verifySignature(req) {
const signature = req.headers['x-zm-signature'];
const timestamp = req.headers['x-zm-request-timestamp'];
if (!signature || !timestamp) {
console.error('Missing signature headers');
return false;
}
// Construct the message
const message = `v0:${timestamp}:${JSON.stringify(req.body)}`;
// Hash the message
const hashForVerify = crypto
.createHmac('sha256', WEBHOOK_SECRET_TOKEN)
.update(message)
.digest('hex');
// Prepend v0=
const computedSignature = `v0=${hashForVerify}`;
// Compare signatures
return signature === computedSignature;
}Signature Headers Example
POST /webhook HTTP/1.1
Host: example.com
x-zm-signature: v0=a05d830fa017433bc47887f835a00b9ff33d3882f22f63a2986a8es270341
x-zm-request-timestamp: 1658940994
Content-Type: application/json
{"event":"meeting.started","payload":{...}}Event Handling
Event Router
function handleEvent(event, payload) {
switch (event) {
case 'meeting.created':
handleMeetingCreated(payload);
break;
case 'meeting.started':
handleMeetingStarted(payload);
break;
case 'meeting.ended':
handleMeetingEnded(payload);
break;
case 'meeting.participant_joined':
handleParticipantJoined(payload);
break;
case 'recording.completed':
handleRecordingCompleted(payload);
break;
default:
console.log(`Unhandled event: ${event}`);
}
}Event Handlers
function handleMeetingStarted(payload) {
const { id, uuid, topic, start_time } = payload.object;
console.log(`Meeting started: ${topic} (ID: ${id})`);
// Your logic: Send notifications, start recording, etc.
// Example: Trigger auto-recording
// await startCloudRecording(id);
}
function handleMeetingEnded(payload) {
const { id, uuid, topic, duration } = payload.object;
console.log(`Meeting ended: ${topic} (Duration: ${duration}min)`);
// Your logic: Process analytics, trigger workflows, etc.
}
function handleRecordingCompleted(payload) {
const { id, uuid, topic, recording_files } = payload.object;
console.log(`Recording ready: ${topic}`);
// Download recordings (see recording-pipeline.md)
recording_files.forEach(file => {
console.log(`- ${file.file_type}: ${file.download_url}`);
// downloadRecording(file.download_url, file.id);
});
}
function handleParticipantJoined(payload) {
const { participant } = payload.object;
console.log(`Participant joined: ${participant.user_name}`);
// Your logic: Track attendance, send welcome message, etc.
}Complete Production Server
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET_TOKEN = process.env.ZOOM_WEBHOOK_SECRET;
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
// Request logging
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
});
// Webhook endpoint
app.post('/webhook', async (req, res) => {
try {
const { event, payload, event_ts } = req.body;
// CRC validation
if (event === 'endpoint.url_validation') {
return handleCRC(req, res);
}
// Verify signature
if (!verifySignature(req)) {
console.error('Signature verification failed');
return res.status(401).send('Unauthorized');
}
// Log event
console.log(`Event received: ${event} at ${new Date(event_ts)}`);
// Handle event asynchronously
setImmediate(() => {
handleEvent(event, payload).catch(error => {
console.error('Error handling event:', error);
});
});
// Respond immediately (within 3 seconds)
res.status(200).send();
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Internal Server Error');
}
});
// CRC validation
function handleCRC(req, res) {
const { plainToken } = req.body.payload;
const encryptedToken = crypto
.createHmac('sha256', WEBHOOK_SECRET_TOKEN)
.update(plainToken)
.digest('hex');
res.status(200).json({ plainToken, encryptedToken });
console.log('CRC validation successful');
}
// Signature verification
function verifySignature(req) {
const signature = req.headers['x-zm-signature'];
const timestamp = req.headers['x-zm-request-timestamp'];
if (!signature || !timestamp) {
return false;
}
const message = `v0:${timestamp}:${JSON.stringify(req.body)}`;
const hashForVerify = crypto
.createHmac('sha256', WEBHOOK_SECRET_TOKEN)
.update(message)
.digest('hex');
const computedSignature = `v0=${hashForVerify}`;
return signature === computedSignature;
}
// Event handler
async function handleEvent(event, payload) {
switch (event) {
case 'meeting.started':
await handleMeetingStarted(payload);
break;
case 'meeting.ended':
await handleMeetingEnded(payload);
break;
case 'recording.completed':
await handleRecordingCompleted(payload);
break;
// Add more event handlers as needed
default:
console.log(`Unhandled event: ${event}`);
}
}
async function handleMeetingStarted(payload) {
console.log(`Meeting started: ${payload.object.topic}`);
// Your business logic
}
async function handleMeetingEnded(payload) {
console.log(`Meeting ended: ${payload.object.topic}`);
// Your business logic
}
async function handleRecordingCompleted(payload) {
console.log(`Recording completed: ${payload.object.topic}`);
// Download logic (see recording-pipeline.md)
}
// Health check
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Start server
app.listen(PORT, () => {
console.log(`Webhook server running on port ${PORT}`);
console.log(`Webhook endpoint: ${process.env.PUBLIC_BASE_URL || 'https://YOUR_PUBLIC_BASE_URL'}/webhook`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
process.exit(0);
});Webhook Retry Policy
Zoom automatically retries failed webhooks 3 times with exponential backoff:
1. First retry: 5 minutes after initial failure 2. Second retry: 20 minutes after first retry 3. Third retry: 60 minutes after second retry
Retry Conditions
Zoom retries for:
- HTTP status codes ≥ 500
- Network errors (connection refused, timeout, etc.)
Zoom does NOT retry for:
- HTTP status codes 200-299 (success)
- HTTP status codes 300-399 (redirects)
- HTTP status codes 400-499 (client errors)
Handling Retries
// Track processed events to avoid duplicate processing
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const { event, event_ts, payload } = req.body;
// Create unique event ID
const eventId = `${event}-${event_ts}-${payload.object?.id || ''}`;
// Check if already processed (duplicate due to retry)
if (processedEvents.has(eventId)) {
console.log(`Duplicate event: ${eventId}`);
return res.status(200).send(); // Still return 200
}
// Mark as processed
processedEvents.add(eventId);
// Handle event
handleEvent(event, payload);
res.status(200).send();
// Clean up old entries after 2 hours
setTimeout(() => processedEvents.delete(eventId), 2 * 60 * 60 * 1000);
});Webhook Revalidation
Zoom automatically revalidates webhook endpoints every 72 hours. If revalidation fails 6 consecutive times, Zoom disables the webhook.
Revalidation Notifications
- 2 failures: First email notification
- 4 failures: Second email notification
- 6 failures: Webhook disabled
Ensure Uptime
// Health check with monitoring
app.get('/health', (req, res) => {
// Check dependencies (database, external APIs, etc.)
const isHealthy = checkDependencies();
if (isHealthy) {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
} else {
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString()
});
}
});
function checkDependencies() {
// Check database connection, external APIs, etc.
return true;
}Environment Variables
# .env
ZOOM_WEBHOOK_SECRET=your_webhook_secret_token_here
PORT=3000
NODE_ENV=productionLoading Environment Variables
require('dotenv').config();
const WEBHOOK_SECRET_TOKEN = process.env.ZOOM_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET_TOKEN) {
throw new Error('ZOOM_WEBHOOK_SECRET environment variable is required');
}Deployment
Requirements
1. HTTPS required - Zoom only sends to HTTPS endpoints 2. Public URL - Endpoint must be publicly accessible 3. TLS 1.2+ - Valid certificate from a Certificate Authority (CA) 4. FQDN - Fully qualified domain name (not IP address) 5. Response time - Respond within 3 seconds
Deployment Options
- Heroku:
git push heroku main - AWS Lambda: Use API Gateway + Lambda function
- Vercel/Netlify: Serverless functions
- Self-hosted: Nginx + Node.js + Let's Encrypt
ngrok for Local Development
# Install ngrok
npm install -g ngrok
# Start your server
node server.js
# In another terminal, expose to public URL
ngrok http 3000
# Use the HTTPS URL in Zoom webhook configuration
# Example: https://abc123.ngrok.io/webhookTesting
Test CRC Validation
WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:3000"
curl -X POST "$WEBHOOK_BASE_URL/webhook" \
-H "Content-Type: application/json" \
-d '{
"event": "endpoint.url_validation",
"payload": {
"plainToken": "test_token_123"
},
"event_ts": 1654503849680
}'Expected response:
{
"plainToken": "test_token_123",
"encryptedToken": "..."
}Test Event Handling
# Generate valid signature
TIMESTAMP=$(date +%s)
MESSAGE="v0:${TIMESTAMP}:{\"event\":\"meeting.started\",\"payload\":{\"object\":{\"id\":\"123\",\"topic\":\"Test\"}}}"
SIGNATURE="v0=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "YOUR_SECRET" -binary | xxd -p)"
WEBHOOK_BASE_URL="http://YOUR_DEV_HOST:3000"
curl -X POST "$WEBHOOK_BASE_URL/webhook" \
-H "Content-Type: application/json" \
-H "x-zm-signature: $SIGNATURE" \
-H "x-zm-request-timestamp: $TIMESTAMP" \
-d '{"event":"meeting.started","payload":{"object":{"id":"123","topic":"Test Meeting"}}}'Common Event Types
| Event | Description |
|---|---|
meeting.created | Meeting created |
meeting.updated | Meeting details changed |
meeting.deleted | Meeting deleted |
meeting.started | Meeting begins |
meeting.ended | Meeting ends |
meeting.participant_joined | Participant joins |
meeting.participant_left | Participant leaves |
recording.completed | Cloud recording ready |
recording.transcript_completed | Transcript ready |
user.created | User created |
user.updated | User updated |
user.deleted | User deleted |
See complete event catalog: webhooks skill
Related Documentation
- [webhooks skill](../../webhooks/SKILL.md) - Comprehensive webhook documentation
- [Recording Pipeline](recording-pipeline.md) - Download recordings from webhook events
- [Meeting Lifecycle](meeting-lifecycle.md) - Create/update/delete meetings
- [Common Issues](../troubleshooting/common-issues.md) - Webhook troubleshooting
Resources
Zoom Accounts API
Authoritative endpoint inventory for Accounts. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/accounts/methods/endpoints.json
- Base URL:
https://api.zoom.us/v2 - Authentication details: authentication.md
Notes
- Endpoint methods and paths below are generated from the official Zoom API Hub
pathsobject. - Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation.
- Use this file for endpoint discovery and inventory. Use
../examples/for orchestration patterns, not as the canonical source of path names.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 59 |
| Path templates | 46 |
| Tags | 6 |
Tag Index
| Tag | Operations |
|---|---|
| Accounts | 11 |
| Dashboards | 26 |
| Data Requests | 5 |
| Information Barriers | 5 |
| Roles | 8 |
| Survey Management | 4 |
Endpoints by Tag
Accounts
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /accounts/{accountId}/lock_settings | Get locked settings | getAccountLockSettings |
| PATCH | /accounts/{accountId}/lock_settings | Update locked settings | UpdateLockedSettings |
| GET | /accounts/{accountId}/managed_domains | Get account's managed domains | accountManagedDomain |
| PUT | /accounts/{accountId}/owner | Update the account owner | UpdateTheAccountOwner |
| GET | /accounts/{accountId}/settings | Get account settings | accountSettings |
| PATCH | /accounts/{accountId}/settings | Update account settings | accountSettingsUpdate |
| GET | /accounts/{accountId}/settings/registration | Get an account's webinar registration settings | accountSettingsRegistration |
| PATCH | /accounts/{accountId}/settings/registration | Update an account's webinar registration settings | accountSettingsRegistrationUpdate |
| DELETE | /accounts/{accountId}/settings/virtual_backgrounds | Delete virtual background files | delVB |
| POST | /accounts/{accountId}/settings/virtual_backgrounds | Upload virtual background files | uploadVB |
| GET | /accounts/{accountId}/trusted_domains | Get account's trusted domains | accountTrustedDomain |
Dashboards
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /metrics/chat | Get chat metrics | dashboardChat |
| GET | /metrics/client/feedback | List Zoom meetings client feedback | dashboardClientFeedback |
| GET | /metrics/client/feedback/{feedbackId} | Get zoom meetings client feedback | dashboardClientFeedbackDetail |
| GET | /metrics/client/satisfaction | List client meeting satisfaction | listMeetingSatisfaction |
| GET | /metrics/client_versions | List the client versions | getClientVersions |
| GET | /metrics/crc | Get CRC port usage | dashboardCRC |
| GET | /metrics/issues/zoomrooms | Get top 25 Zoom Rooms with issues | dashboardIssueZoomRoom |
| GET | /metrics/issues/zoomrooms/{zoomroomId} | Get issues of Zoom Rooms | dashboardIssueDetailZoomRoom |
| GET | /metrics/meetings | List meetings | dashboardMeetings |
| GET | /metrics/meetings/{meetingId} | Get meeting details | dashboardMeetingDetail |
| GET | /metrics/meetings/{meetingId}/participants | List meeting participants | dashboardMeetingParticipants |
| GET | /metrics/meetings/{meetingId}/participants/qos | List meeting participants QoS | dashboardMeetingParticipantsQOS |
| GET | /metrics/meetings/{meetingId}/participants/satisfaction | Get post meeting feedback | participantFeedback |
| GET | /metrics/meetings/{meetingId}/participants/sharing | Get meeting sharing/recording details | dashboardMeetingParticipantShare |
| GET | /metrics/meetings/{meetingId}/participants/{participantId}/qos | Get meeting participant QoS | dashboardMeetingParticipantQOS |
| GET | /metrics/quality | Get meeting quality scores | dashboardQuality |
| GET | /metrics/webinars | List webinars | dashboardWebinars |
| GET | /metrics/webinars/{webinarId} | Get webinar details | dashboardWebinarDetail |
| GET | /metrics/webinars/{webinarId}/participants | Get webinar participants | dashboardWebinarParticipants |
| GET | /metrics/webinars/{webinarId}/participants/qos | List webinar participant QoS | dashboardWebinarParticipantsQOS |
| GET | /metrics/webinars/{webinarId}/participants/satisfaction | Get post webinar feedback | participantWebinarFeedback |
| GET | /metrics/webinars/{webinarId}/participants/sharing | Get webinar sharing/recording details | dashboardWebinarParticipantShare |
| GET | /metrics/webinars/{webinarId}/participants/{participantId}/qos | Get webinar participant QoS | dashboardWebinarParticipantQOS |
| GET | /metrics/zoomrooms | List Zoom Rooms | dashboardZoomRooms |
| GET | /metrics/zoomrooms/issues | Get top 25 issues of Zoom Rooms | dashboardZoomRoomIssue |
| GET | /metrics/zoomrooms/{zoomroomId} | Get Zoom Rooms details | dashboardZoomRoom |
Data Requests
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /data_requests/files/{fileId}/url | Get download link for data access request file | DownloadfilesfromDataRequest |
| GET | /data_requests/requests | List data request history | GetDataRequestsHistory |
| POST | /data_requests/requests | Create data (export/deletion) request | CreateDataAccessRequest |
| DELETE | /data_requests/requests/{requestId} | Cancel data deletion request | CancelDataRequest |
| GET | /data_requests/requests/{requestId} | List downloadable files for export data request | GetDownloadableFilesforDataRequest |
Information Barriers
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /information_barriers/policies | List information Barrier policies | InformationBarriersList |
| POST | /information_barriers/policies | Create an Information Barrier policy | InformationBarriersCreate |
| DELETE | /information_barriers/policies/{policyId} | Remove an Information Barrier policy | InformationBarriersDelete |
| GET | /information_barriers/policies/{policyId} | Get an Information Barrier policy by ID | InformationBarriersGet |
| PATCH | /information_barriers/policies/{policyId} | Update an Information Barriers policy | InformationBarriersUpdate |
Roles
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /roles | List roles | roles |
| POST | /roles | Create a role | createRole |
| DELETE | /roles/{roleId} | Delete a role | deleteRole |
| GET | /roles/{roleId} | Get role information | getRoleInformation |
| PATCH | /roles/{roleId} | Update role information | updateRole |
| GET | /roles/{roleId}/members | List members in a role | roleMembers |
| POST | /roles/{roleId}/members | Assign a role | AddRoleMembers |
| DELETE | /roles/{roleId}/members/{memberId} | Unassign a role | roleMemberDelete |
Survey Management
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /surveys | Get surveys | getAccountSurveys |
| GET | /surveys/{surveyId} | Get survey info | getSurveyInfo |
| GET | /surveys/{surveyId}/answers | Get survey answers | getSurveyAnswers |
| GET | /surveys/{surveyId}/instances | Get survey instances | getSurveyInstancesInfo |
Zoom AI Companion API
Authoritative endpoint inventory for AI Companion. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/ai-companion/methods/endpoints.json
- Base URL:
https://api.zoom.us/v2 - Authentication details: authentication.md
Notes
- Endpoint methods and paths below are generated from the official Zoom API Hub
pathsobject. - Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation.
- Use this file for endpoint discovery and inventory. Use
../examples/for orchestration patterns, not as the canonical source of path names.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 1 |
| Path templates | 1 |
| Tags | 1 |
Tag Index
| Tag | Operations |
|---|---|
| Archive | 1 |
Endpoints by Tag
Archive
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /aic/users/{userId}/conversation_archive | Get AI Companion conversation archives | GetAICconversationarchives |
Zoom AI Services API
Authoritative endpoint inventory for AI Services / Scribe. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/ai-services/methods/endpoints.json
- Base URL:
https://api.zoom.us/v2 - Product skill: ../../scribe/SKILL.md
- Authentication details: authentication.md
Notes
- Current OpenAPI surface is Scribe-focused.
- Auth uses Build-platform JWT, which differs from the standard OAuth-centric paths in most REST API product areas.
- Use this file for endpoint discovery and inventory. Use the
scribeskill for workflow, webhook, and mode-selection guidance.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 6 |
| Path templates | 4 |
| Tags | 1 |
Tag Index
| Tag | Operations |
|---|---|
| Scribe | 6 |
Endpoints by Tag
Scribe
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /aiservices/scribe/jobs | List Batch Jobs | listBatchJobs |
| POST | /aiservices/scribe/jobs | Submit Batch Scribe Job | submitBatchAsr |
| GET | /aiservices/scribe/jobs/{jobId} | Get Batch Job Status | getBatchJobStatus |
| DELETE | /aiservices/scribe/jobs/{jobId} | Cancel Batch Job | cancelBatchJob |
| GET | /aiservices/scribe/jobs/{jobId}/files | List Batch Job Files | listBatchJobFiles |
| POST | /aiservices/scribe/transcribe | Scribe (Synchronous) | createFastAsr |
Authentication Guide
Comprehensive guide to Zoom API authentication methods: OAuth 2.0, Server-to-Server OAuth, and JWT (legacy).
Overview
Zoom APIs support multiple authentication methods depending on your use case:
| Method | Use Case | Token Lifetime |
|---|---|---|
| User OAuth 2.0 | Act on behalf of users | 1 hour (refresh: 15 years) |
| Server-to-Server OAuth | Backend automation | 1 hour |
| JWT (Deprecated) | Legacy integrations | Custom |
Server-to-Server OAuth (Recommended for Backend)
For backend services that don't need user interaction.
Setup
1. Go to Zoom App Marketplace 2. Click Develop → Build App 3. Select Server-to-Server OAuth 4. Note your credentials:
- Account ID
- Client ID
- Client Secret
Get Access Token
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n '{clientId}:{clientSecret}' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=account_credentials&account_id={accountId}"Response
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "meeting:read meeting:write user:read"
}Code Example (Node.js)
const axios = require('axios');
class ZoomAuth {
constructor(accountId, clientId, clientSecret) {
this.accountId = accountId;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.token = null;
this.tokenExpiry = null;
}
async getAccessToken() {
// Return cached token if still valid
if (this.token && this.tokenExpiry > Date.now() + 60000) {
return this.token;
}
const credentials = Buffer.from(
`${this.clientId}:${this.clientSecret}`
).toString('base64');
const response = await axios.post(
'https://zoom.us/oauth/token',
`grant_type=account_credentials&account_id=${this.accountId}`,
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.token = response.data.access_token;
this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
return this.token;
}
async apiRequest(method, endpoint, data = null) {
const token = await this.getAccessToken();
const config = {
method,
url: `https://api.zoom.us/v2${endpoint}`,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
if (data) {
config.data = data;
}
return axios(config);
}
}
// Usage
const zoom = new ZoomAuth(
process.env.ZOOM_ACCOUNT_ID,
process.env.ZOOM_CLIENT_ID,
process.env.ZOOM_CLIENT_SECRET
);
const users = await zoom.apiRequest('GET', '/users');User OAuth 2.0 (For User Actions)
For applications that act on behalf of individual users.
OAuth Flow
1. User clicks "Connect to Zoom"
↓
2. Redirect to Zoom authorization URL
↓
3. User grants permission
↓
4. Zoom redirects to your callback with code
↓
5. Exchange code for access token
↓
6. Use access token for API calls
↓
7. Refresh token when expiredStep 1: Create OAuth App
1. Go to Zoom App Marketplace 2. Click Develop → Build App 3. Select OAuth 4. Configure:
- Redirect URI(s)
- Required scopes
- App information
Step 2: Authorization URL
Redirect users to:
https://zoom.us/oauth/authorize?response_type=code&client_id={clientId}&redirect_uri={redirectUri}&state={state}| Parameter | Description |
|---|---|
response_type | Always code |
client_id | Your OAuth app client ID |
redirect_uri | Must match registered URI |
state | Random string for CSRF protection |
Step 3: Handle Callback
User is redirected to your callback URL:
https://yourapp.com/callback?code=AUTH_CODE&state=STATEStep 4: Exchange Code for Token
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n '{clientId}:{clientSecret}' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code&code={authCode}&redirect_uri={redirectUri}"Response
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiJ9...",
"expires_in": 3600,
"scope": "meeting:read meeting:write user:read"
}Step 5: Refresh Token
curl -X POST "https://zoom.us/oauth/token" \
-H "Authorization: Basic $(echo -n '{clientId}:{clientSecret}' | base64)" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token&refresh_token={refreshToken}"Complete OAuth Example (Express.js)
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');
const app = express();
const ZOOM_CLIENT_ID = process.env.ZOOM_CLIENT_ID;
const ZOOM_CLIENT_SECRET = process.env.ZOOM_CLIENT_SECRET;
const REDIRECT_URI = 'https://yourapp.com/auth/zoom/callback';
// Step 2: Initiate OAuth
app.get('/auth/zoom', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
const authUrl = new URL('https://zoom.us/oauth/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', ZOOM_CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('state', state);
res.redirect(authUrl.toString());
});
// Step 3 & 4: Handle callback and exchange code
app.get('/auth/zoom/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state
if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}
try {
// Exchange code for token
const credentials = Buffer.from(
`${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}`
).toString('base64');
const tokenResponse = await axios.post(
'https://zoom.us/oauth/token',
`grant_type=authorization_code&code=${code}&redirect_uri=${REDIRECT_URI}`,
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
const { access_token, refresh_token, expires_in } = tokenResponse.data;
// Store tokens securely (database)
await storeTokens(req.user.id, {
accessToken: access_token,
refreshToken: refresh_token,
expiresAt: Date.now() + (expires_in * 1000)
});
res.redirect('/dashboard');
} catch (error) {
console.error('OAuth error:', error.response?.data);
res.status(500).send('Authentication failed');
}
});
// Helper: Get valid access token (refresh if needed)
async function getValidToken(userId) {
const tokens = await getStoredTokens(userId);
// Check if token is expired (with 1 min buffer)
if (tokens.expiresAt < Date.now() + 60000) {
const credentials = Buffer.from(
`${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}`
).toString('base64');
const response = await axios.post(
'https://zoom.us/oauth/token',
`grant_type=refresh_token&refresh_token=${tokens.refreshToken}`,
{
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
await storeTokens(userId, {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token,
expiresAt: Date.now() + (response.data.expires_in * 1000)
});
return response.data.access_token;
}
return tokens.accessToken;
}Scopes
Common Scopes
| Scope | Description |
|---|---|
user:read | Read user profile |
user:write | Update user profile |
meeting:read | Read meeting data |
meeting:write | Create/update meetings |
recording:read | Access recordings |
recording:write | Manage recordings |
phone:read | Read Zoom Phone data |
phone:write | Manage Zoom Phone |
Admin Scopes
| Scope | Description |
|---|---|
user:read:admin | Read all users |
user:write:admin | Manage all users |
meeting:read:admin | Read all meetings |
account:read:admin | Read account settings |
Scope Selection
Request only scopes you need:
- More scopes = more user friction
- Less scopes = better approval chance
- Add scopes incrementally as features grow
Token Storage Best Practices
// DO: Encrypt tokens at rest
const encryptedToken = encrypt(accessToken, encryptionKey);
await db.tokens.save({ userId, encryptedToken });
// DO: Use secure, httpOnly cookies for web apps
res.cookie('zoom_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});
// DON'T: Store tokens in localStorage
localStorage.setItem('zoom_token', token); // BAD
// DON'T: Log tokens
console.log('Token:', accessToken); // BADError Handling
Common OAuth Errors
| Error | Cause | Solution |
|---|---|---|
invalid_grant | Expired/used code | Restart OAuth flow |
invalid_client | Wrong credentials | Check client ID/secret |
invalid_scope | Unauthorized scope | Request only approved scopes |
access_denied | User denied permission | Handle gracefully |
Token Refresh Errors
try {
const newToken = await refreshToken(refreshToken);
} catch (error) {
if (error.response?.data?.error === 'invalid_grant') {
// Refresh token expired or revoked
// User needs to re-authorize
redirectToOAuth();
}
}Webhook Verification
For webhook security, verify the request signature:
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`;
const signature = crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
const expected = `v0=${signature}`;
return req.headers['x-zm-signature'] === expected;
}
app.post('/webhooks/zoom', (req, res) => {
if (!verifyWebhook(req, process.env.ZOOM_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const event = req.body;
// ...
});Migration from JWT (Deprecated)
JWT apps are deprecated. Migrate to Server-to-Server OAuth:
1. Create new Server-to-Server OAuth app 2. Request same scopes 3. Update code to use OAuth token endpoint 4. Test thoroughly 5. Delete JWT app
// OLD (JWT - Deprecated)
const token = jwt.sign(payload, apiSecret);
// NEW (Server-to-Server OAuth)
const token = await getServerToServerToken();Resources
- OAuth Guide: https://developers.zoom.us/docs/integrations/oauth/
- Server-to-Server OAuth: https://developers.zoom.us/docs/internal-apps/s2s-oauth/
- Scopes Reference: https://developers.zoom.us/docs/integrations/oauth-scopes/
- Migration Guide: https://developers.zoom.us/docs/internal-apps/jwt-app-migration/
Zoom Auto Dialer API
Authoritative endpoint inventory for Auto Dialer. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/auto-dialer/methods/endpoints.json
- Base URL:
https://api.zoom.us/v2 - Authentication details: authentication.md
Notes
- Endpoint methods and paths below are generated from the official Zoom API Hub
pathsobject. - Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation.
- Use this file for endpoint discovery and inventory. Use
../examples/for orchestration patterns, not as the canonical source of path names.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 14 |
| Path templates | 8 |
| Tags | 3 |
Tag Index
| Tag | Operations |
|---|---|
| Call History & Reporting | 2 |
| Call List Management | 5 |
| Prospect Management | 7 |
Endpoints by Tag
Call History & Reporting
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /dialer/call-histories/{callHistoryId} | Get Call History by ID | GetCallDetailsbyCallID |
| GET | /dialer/call-history | Get Call History | GetCallHistory |
Call List Management
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /dialer/call-lists | List Call Lists | ListCallLists |
| POST | /dialer/call-lists | Create Call List | CreateCallList |
| DELETE | /dialer/call-lists/{callListId} | Delete Call List | DeleteCallList |
| GET | /dialer/call-lists/{callListId} | Get Call List by ID | GetCallListbyID |
| PATCH | /dialer/call-lists/{callListId} | Update Call List | UpdateCallList |
Prospect Management
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /dialer/call-lists/{callListId}/prospects | List All Prospects in Call List | ListAllProspectsInCallList |
| PATCH | /dialer/call-lists/{callListId}/prospects | Update Prospects batch | UpdateProspects |
| POST | /dialer/call-lists/{callListId}/prospects | Create Prospect | CreateProspect |
| POST | /dialer/call-lists/{callListId}/prospects/batch | Create Prospects batch | CreateProspects |
| DELETE | /dialer/call-lists/{callListId}/prospects/{prospectId} | Delete Prospect | DeleteProspect |
| PATCH | /dialer/call-lists/{callListId}/prospects/{prospectId} | Update Prospect | UpdateProspect |
| GET | /dialer/prospects/{prospectId} | Get Prospect by ID | GetProspectbyID |
Zoom Calendar API
Authoritative endpoint inventory for Calendar. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/calendar/methods/endpoints.json
- Base URL:
https://api.zoom.us/v2 - Authentication details: authentication.md
Notes
- Endpoint methods and paths below are generated from the official Zoom API Hub
pathsobject. - Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation.
- Use this file for endpoint discovery and inventory. Use
../examples/for orchestration patterns, not as the canonical source of path names.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 28 |
| Path templates | 16 |
| Tags | 7 |
Tag Index
| Tag | Operations |
|---|---|
| acl | 5 |
| calendar list | 5 |
| calendars | 4 |
| colors | 1 |
| events | 9 |
| freebusy | 1 |
| settings | 3 |
Endpoints by Tag
acl
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /calendars/{calId}/acl | List ACL rules of specified calendar | Listacl |
| POST | /calendars/{calId}/acl | Create a new ACL rule | Insertacl |
| DELETE | /calendars/{calId}/acl/{aclId} | Delete an existing ACL rule | Deleteacl |
| GET | /calendars/{calId}/acl/{aclId} | Get the specified ACL rule | Getacl |
| PATCH | /calendars/{calId}/acl/{aclId} | Update the specified ACL rule | Patchacl |
calendar list
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /calendars/users/{userIdentifier}/calendarList | List the calendars in the user's own calendarList | ListcalendarList |
| POST | /calendars/users/{userIdentifier}/calendarList | Insert an existing calendar to the user's own calendarList | InsertcalendarList |
| DELETE | /calendars/users/{userIdentifier}/calendarList/{calendarId} | Delete an existing calendar from the user's own calendarList | DeletecalendarList |
| GET | /calendars/users/{userIdentifier}/calendarList/{calendarId} | Get a specified calendar from the user's own calendarList | GetcalendarList |
| PATCH | /calendars/users/{userIdentifier}/calendarList/{calendarId} | Update an existing calendar in the user's own calendarList | PatchcalendarList |
calendars
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /calendars | Create a new secondary calendar | Insertcalendar |
| DELETE | /calendars/{calId} | Delete a calendar owned by a user | Deletecalendar |
| GET | /calendars/{calId} | Get the specified calendar | Getcalendar |
| PATCH | /calendars/{calId} | Update the specified calendar | Patchcalendar |
colors
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /calendars/colors | Get the color definitions for calendars and events | Getcolor |
events
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /calendars/{calId}/events | List events on the specified calendar | Listevent |
| POST | /calendars/{calId}/events | Insert a new event to the specified calendar | Insertevent |
| POST | /calendars/{calId}/events/import | Import event to the specified calendar | Importevent |
| POST | /calendars/{calId}/events/quickAdd | Quick add an event to the specified calendar | Quickaddevent |
| DELETE | /calendars/{calId}/events/{eventId} | Delete an existing event from the specified calendar | Deleteevent |
| GET | /calendars/{calId}/events/{eventId} | Get the specified event on the specified calendar | Getevent |
| PATCH | /calendars/{calId}/events/{eventId} | Update the specified event on the specified calendar | Patchevent |
| GET | /calendars/{calId}/events/{eventId}/instances | List all instances of the specified recurring event | Instanceevent |
| POST | /calendars/{calId}/events/{eventId}/move | Move the specified event from a calendar to another specified calendar | Moveevent |
freebusy
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /calendars/freeBusy | Query freebusy information for a set of calendars | Queryfreebusy |
settings
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /calendars/users/{userIdentifier}/settings | List all user calendar settings of the authenticated user | Listsettings |
| GET | /calendars/users/{userIdentifier}/settings/{settingId} | Get the specified user calendar settings of the authenticated user | Getsetting |
| PATCH | /calendars/users/{userIdentifier}/settings/{settingId} | Patch the specified user calendar settings of the authenticated user | Patchsetting |
Zoom Chatbot API
Authoritative endpoint inventory for Chatbot. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/chatbot/methods/endpoints.json
- Base URL:
https://api.zoom.us/v2 - Authentication details: authentication.md
Notes
- Endpoint methods and paths below are generated from the official Zoom API Hub
pathsobject. - Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation.
- Use this file for endpoint discovery and inventory. Use
../examples/for orchestration patterns, not as the canonical source of path names.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 4 |
| Path templates | 3 |
| Tags | 1 |
Tag Index
| Tag | Operations |
|---|---|
| Chatbot Messages | 4 |
Endpoints by Tag
Chatbot Messages
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /im/chat/messages | Send Chatbot messages | sendChatbot |
| DELETE | /im/chat/messages/{message_id} | Delete a Chatbot message | deleteAChatbotMessage |
| PUT | /im/chat/messages/{message_id} | Edit a Chatbot message | editChatbotMessage |
| POST | /im/chat/users/{userId}/unfurls/{triggerId} | Link Unfurls | unfurlingLink |
Zoom Clips API
Authoritative endpoint inventory for Clips. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/clips/methods/endpoints.json
- Base URL:
https://api.zoom.us/ - Authentication details: authentication.md
Notes
- Endpoint methods and paths below are generated from the official Zoom API Hub
pathsobject. - Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation.
- Use this file for endpoint discovery and inventory. Use
../examples/for orchestration patterns, not as the canonical source of path names.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 13 |
| Path templates | 11 |
| Tags | 7 |
Tag Index
| Tag | Operations |
|---|---|
| Clips | 3 |
| Collaborator | 1 |
| Comment | 2 |
| Download | 1 |
| Single | 1 |
| Transfer | 2 |
| Upload | 3 |
Endpoints by Tag
Clips
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /clips | List all clips | GetUserClips |
| GET | /clips/{clipId} | Get a clip | GetClipById |
| GET | /clips/{clipId}/collaborators | Get collaborators of a clip | GetClipCollaborators |
Collaborator
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| DELETE | /clips/{clipId}/collaborators | Remove the collaborator from a clip | DeleteCollaborator |
Comment
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /clips/{clipId}/comments | List clip comments | Listclipcomments |
| DELETE | /clips/{clipId}/comments/{commentId} | Delete a comment | Deleteacomment |
Download
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /clips/{clipId}/download | Download a clip | downloadClip |
Single
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| DELETE | /clips/{clipId} | Delete a clip(soft delete) | DeleteClip |
Transfer
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /clips/transfers | Transfer clips owner | Transferclipsowner |
| GET | /clips/transfers/{taskId} | Transfer task status check | Transfertaskstatuscheck |
Upload
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /clips/files | Upload clip file | UploadClipFile |
| POST | /clips/files/multipart | Upload clip multipart files | UploadIqMultipartClipFile |
| POST | /clips/files/multipart/upload_events | Initiate and complete the multipart file upload for a clip | InitiateAndCompleteAClipMultipartUpload. |
Zoom Cobrowse SDK API
Authoritative endpoint inventory for Cobrowse SDK. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/cobrowse-sdk/methods/endpoints.json
- Base URL:
https://api.zoom.us/v2 - Authentication details: authentication.md
Notes
- Endpoint methods and paths below are generated from the official Zoom API Hub
pathsobject. - Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation.
- Use this file for endpoint discovery and inventory. Use
../examples/for orchestration patterns, not as the canonical source of path names.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 4 |
| Path templates | 4 |
| Tags | 1 |
Tag Index
| Tag | Operations |
|---|---|
| Sessions | 4 |
Endpoints by Tag
Sessions
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /cobrowsesdk/live_sessions | List live sessions | Listlivesessions |
| GET | /cobrowsesdk/past_sessions | List past sessions | Listpastsession |
| GET | /cobrowsesdk/sessions/{sessionId} | Get session details | Getasession |
| GET | /cobrowsesdk/sessions/{sessionId}/users | List session users | Listsessionusers |
Zoom Commerce API
Authoritative endpoint inventory for Commerce. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/commerce/methods/endpoints.json
- Base URL:
https://api.zoom.us/v2 - Authentication details: authentication.md
Notes
- Endpoint methods and paths below are generated from the official Zoom API Hub
pathsobject. - Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation.
- Use this file for endpoint discovery and inventory. Use
../examples/for orchestration patterns, not as the canonical source of path names.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 33 |
| Path templates | 31 |
| Tags | 8 |
Tag Index
| Tag | Operations |
|---|---|
| Account Management | 4 |
| Billing | 3 |
| Deal Registration | 5 |
| Order | 4 |
| Platform | 3 |
| Product Catalog | 3 |
| Quote | 6 |
| Subscription | 5 |
Endpoints by Tag
Account Management
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /commerce/account | Create an end customer account | createAccount |
| POST | /commerce/account/{accountKey}/contacts | Add contacts to an existing end customer or your own account. | addAccountContact |
| GET | /commerce/accounts | Get the list of all accounts associated with a Zoom Partner/Sub-Reseller, by the account type | getAllAccounts |
| GET | /commerce/accounts/{accountKey} | Get the account details for a Zoom Partner/Subreseller/End Customer | getAccountDetails |
Billing
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /commerce/billing_documents | Gets all billing documents for a distributor or a reseller | getAllBillingDocs |
| GET | /commerce/billing_documents/{documentNumber}/document | Gets the PDF document for the billing document ID | downloadBillingDoc |
| GET | /commerce/invoices/{invoiceNumber} | Get detailed information about a specific invoice for a distributor or a reseller | getInvoiceDetail |
Deal Registration
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /commerce/campaigns | Retrieves all valid Zoom Campaigns which a deal registration can be associated with. | getCampaigns |
| POST | /commerce/deal_registration | Creates a new deal registration for a partner | createDealReg |
| GET | /commerce/deal_registrations | Gets all valid Deal Registrations for a partner | getAllDealRegs |
| GET | /commerce/deal_registrations/{dealRegKey} | Get details of a deal registration by registration number | getDealRegDetails |
| PATCH | /commerce/deal_registrations/{dealRegKey} | Updates an existing deal registration | Updatesanexistingdealregistration |
Order
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /commerce/order | Create a subscription order for a Zoom partner | createOrder |
| POST | /commerce/order/preview | Preview delta order metrics and subscriptions in an order | createOrderPreview |
| GET | /commerce/orders | Gets all orders for a Zoom partner. | getAllOrders |
| GET | /commerce/orders/{orderReferenceId} | Get order details by order reference ID | getOrderDetails |
Platform
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /commerce/file | Upload an attachment pdf file in context of a deal registration or quote | uploadFile |
| GET | /commerce/files/{associatedReferenceId}/details | Gets details of all files associated with a quote or deal registration | allFileDetails |
| GET | /commerce/files/{documentReferenceId} | Download a file associated with a quote or deal registration. | downloadFile. |
Product Catalog
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /commerce/catalog | Gets Zoom Product Catalog for a Zoom Partner | getOffers |
| GET | /commerce/catalog/{offerId} | Gets the details for a Zoom product or offer. | getOfferDetail |
| GET | /commerce/pricebooks | Gets the pricebook in a downloadable file | downloadPricebook |
Quote
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| POST | /commerce/quote | Create a subscription quote for a Zoom Partner | createQuote |
| POST | /commerce/quote/preview | Preview delta quote metrics and subscriptions in a quote | createQuotePreview |
| GET | /commerce/quotes | Gets all quotes for a Zoom partner | getAllQuotes |
| GET | /commerce/quotes/{quoteReferenceId} | Get quote details by quote reference ID | getQuoteDetails |
| PATCH | /commerce/quotes/{quoteReferenceId} | Update a subscription quote for a Zoom partner | updateQuote |
| PATCH | /commerce/quotes/{quoteReferenceId}/fulfillment | Submits a subscription quote for provisioning | provisionQuote |
Subscription
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /commerce/subscriptions | Gets paid subscriptions for a Zoom partner. | getAllSubscriptions |
| GET | /commerce/subscriptions/{subscriptionNumber} | Gets subscription details for a given subscription number | getSubscriptionDetails |
| GET | /commerce/subscriptions/{subscriptionNumber}/versions | Gets subscription changes/versions for a given subscription number. | getSubscriptionVersions |
| GET | /commerce/trials | Get trial subscriptions for a Zoom partner | getAllTrialSubscriptions |
| GET | /commerce/trials/{trialReferenceId} | Get trial details for an end customer by their Zoom account number or the trial ID | getTrialDetails |
Zoom REST API Environment Variables
Standard .env keys
| Variable | Required | Used for | Where to find |
|---|---|---|---|
ZOOM_CLIENT_ID | Yes | OAuth app identity for API access | Zoom Marketplace -> OAuth app -> App Credentials |
ZOOM_CLIENT_SECRET | Yes | OAuth app secret | Zoom Marketplace -> OAuth app -> App Credentials |
ZOOM_ACCOUNT_ID | S2S OAuth mode | Account token grant | Zoom Marketplace -> Server-to-Server OAuth app credentials |
ZOOM_REDIRECT_URI | User OAuth mode | Authorization callback URL | Zoom Marketplace -> OAuth redirect/allow list |
ZOOM_WEBHOOK_SECRET | If receiving events | Signature validation for webhook events | Zoom Marketplace -> Event Subscriptions -> Secret Token |
Runtime-only values
ZOOM_ACCESS_TOKENZOOM_REFRESH_TOKEN
Notes
- Use
ZOOM_ACCOUNT_IDfor server-to-server service integrations. - User-level integrations require authorization code flow and
ZOOM_REDIRECT_URI.
Zoom Healthcare API
Authoritative endpoint inventory for Healthcare. This file mirrors the official Zoom API Hub OpenAPI document for this product area.
Canonical Source
- OpenAPI JSON: https://developers.zoom.us/api-hub/healthcare/methods/endpoints.json
- Base URL:
https://api.zoom.us/v2 - Authentication details: authentication.md
Notes
- Endpoint methods and paths below are generated from the official Zoom API Hub
pathsobject. - Scope names are defined per operation and frequently use granular scope names. Check the API Hub operation page for the exact scopes before implementation.
- Use this file for endpoint discovery and inventory. Use
../examples/for orchestration patterns, not as the canonical source of path names.
Coverage
| Metric | Value |
|---|---|
| Endpoint operations | 3 |
| Path templates | 2 |
| Tags | 1 |
Tag Index
| Tag | Operations |
|---|---|
| clinicalnotes | 3 |
Endpoints by Tag
clinicalnotes
| Method | Endpoint | Summary | Operation ID |
|---|---|---|---|
| GET | /clinical_notes/notes | List clinical notes | GetClinicalNote |
| GET | /clinical_notes/notes/{noteId} | Get a Clinical Note | GetaClinicalNote |
| PATCH | /clinical_notes/notes/{noteId} | Update a Clinical Note | UpdateClinicalNote |
Related skills
How it compares
Pick build-zoom-rest-api-app over plan-zoom-product when REST or GraphQL implementation details—not surface selection—are the blocker.
FAQ
What does build-zoom-rest-api-app do?
Reference skill for Zoom REST API. Use after choosing an API-based workflow when you need endpoint selection, resource-management patterns, OAuth requirements, rate-limit.
When should I use build-zoom-rest-api-app?
User asks about build zoom rest api app or related SKILL.md workflows.
Is build-zoom-rest-api-app safe to install?
Review the Security Audits panel on this page before installing in production.